Quantcast
Channel: Cybozu Inside Out | サイボウズエンジニアのブログ
Viewing all 694 articles
Browse latest View live

cybozu.com 稼働状況 を React/Redux で作り直した話

$
0
0

こんにちは。Sales Systemチームの金子です。Sales Systemチームでは、cybozu.com Store や、販売管理システム等の開発をしています。

このエントリでは、cybozu.com 稼働状況のフロントエンドをReact/Reduxで作り直した話を書いていきます。「React/ReduxでWebアプリケーションを作ってみようと考えている人」を対象としています。

TOC

  • 「cybozu.com 稼働状況」とは?
  • 作り直した背景
  • 技術概要

    • React/Fluxについて
    • React/Redux
    • Routing
    • Resources
    • Async
    • Multilingualization/Localization
    • ES6
    • Utility
    • Lint
    • Testing
  • 取り組んでみた感想

  • まとめ

「cybozu.com 稼働状況」とは?

クラウドサービスはサービスの稼働状況をステータスダッシュボード形式で提供するのが一般的です。 cybozu.com 稼働状況は、弊社が提供しているcybozu.comで過去30日間で発生した障害内容を表示するサービスです。

記事公開時点ではReact/Reduxで置き換えたcybozu.com稼働状況の運用は始まっておらず、運用開始までに画面イメージを含む仕様変更を行う可能性があります。リリース日は記事公開時点では確定しておりません。

作り直した背景

当初、cybozu.com 稼働状況はインフラチームで開発していました。しかし、インフラチームにはWebアプリのUI開発が得意なメンバーは少なく、また本業であるクリティカルなインフラ運用業務も抱えているため、ビジネスサイドからのUI改善要求にはなかなか対応できませんでした。

そこで、インフラチームは障害情報を提供するサーバAPIのみを担当し、フロントエンド部分はUI開発に慣れているアプリ開発チームが担当するという分担でcybozu.com 稼働状況を再構築することになり、フロントエンド担当としてkintoneチームの天野(@ama_ch)、Sales Systemの私、の二人に話が来ました。

技術概要

弊社は、Google Closure Library/Compilerのイメージが強いと思われますが、今回のような規模のそれほど大きくならないと予想されるアプリケーションにおいては、Google Closure Toolsは少々大げさに感じていました。また、Closure Libraryよりも後発のデータバイディング機能をもつフレームワークのUI開発効率にも魅力を感じていました。

cybozu.com は3か国でサービスを提供しています。そのため、cybozu.com 稼働状況も同様に3か国で提供する必要があります。また、作り直した背景にも書いたように、UI変更に柔軟に対応できる作りにしたいというのがありました。まとめると、cybozu.com 稼働状況フロントエンド部分への要件は以下のようになります。

  • 同じサービスに3種類(3か国)のUIを作成
  • 低コストでUIの変更ができる
  • ルーティングやサーバからのデータ取得をフロントエンドで完結
  • Closure Libraryほどではないが、ある程度の堅さ

そこで、天野がkintoneチームで導入していたReactと、Fluxフレームワークをメインに、npmエコシステムを使って開発しようということになりました。

React/Fluxについて

React/Fluxについては、様々な記事が書かれているので詳細は書きませんが特徴をいくつか挙げます。

  • Viewに相当するComponentと呼ばれるUIパーツ
    • 親Componentから渡されたpropsをもとにrenderingを行う
    • 関心事は基本renderingのみ
    • stateless(であるべき)
    • JSXによる宣言的なDOMの木構造定義
  • Flux architectureに基づくone-way data flow
    • UIとユーザインタラクションの関心事を分離
    • 状態遷移をさせるロジックを局所化し、データの流れを追いやすくする
  • one-way data flowのシンプルさに伴うrenderingコストを改善するためのVirtual DOMアルゴリズム
  • 関数型プログラミングに強い影響を受けている

React/Redux

React/Reduxをメインのフレームワークに採用しました。React/Reduxを採用した理由は以下の通りです。

  • JSXによりViewレイヤーにロジックが入らず、UI変更コストがマークアップ並に抑えられる
  • 簡単にルーティングを含むシングルページアプリケーションが実装できる
  • Reactコンポーネントをうまく差し替えれば少ない工数でUIを大幅に変えることが可能
  • pure Fluxより手軽に乗れるフレームワークがほしかった
  • 各所で評判が良い
  • loggerなど周辺ツールが充実してる
"dependencies": {"react": "^0.13.3",
  "react-redux": "^3.0.0",
  "redux": "^3.0.0"}

React 0.14からはライブラリ構成が変わっているので、少し注意が必要です。

Routing

作り直す前は、サーバで処理したテンプレートを返すものでしたが、SPA(シングルページアプリケーション)で作り直すことにしました。以下のライブラリを使いました。

"dependencies": {"history": "^1.11.1",
  "react-router": "^1.0.0-rc1"}

これらを使うと、React component等を用いて、数行でBrowser history APIを操作することができます。 こんなイメージになります。

index.js

const history = createBrowserHistory();
const store = createStore(dashboard);

React.render(
  <Provider store={store}>
    {() =>
      <Router history={history}>
          <Route component={App} path="/">
          <Route component={App} path="/status/" />
          <Route component={App} path="/status/:subdomain" />
        </Route>
      </Router>
    }</Provider>,
  document.body
);

App.js

class App extends Component {
  handleSubmit(evt) {
    evt.preventDefault();
    const{ subdomain } = this.props;
    this.context.history.pushState(null, `/status/${subdomain}`);
  }
  
  render() {return (
      // blah
    );
  }}

App.contextTypes = {
  history: RouterPropTypes.history
};

Resources

cybozu.com 稼働状況では、ユーザが入力したサブドメインをBrowserに記憶させておき、次回アクセス時にはその値を使用してステータス状況を表示するという仕組みになっています。作り直す前はCookieを使用していましたが、localStorageを使うように変更しました。使ったライブラリは次のようになります。

"dependencies": {"redux-localstorage": "^1.0.0-rc4",
  "redux-localstorage-filter": "^0.1.1"}

redux-localstorageを使うと、StoreのstateとlocalStorageのデータを同期することができます。localStorageによらず、redux-localstorageが定義するstorageのインタフェースを満たしていれば、理論上はなんでも同期できます。詳細は、adaptersのsrcを参照してみてください。

stateのすべてのプロパティをlocalStorageと同期したいわけではないので、redux-localstorage-filterを用いて、同期するプロパティをフィルタリングしています。

// state: dashboardのうち、subdomainプロパティだけlocal storageと同期するconst storage = compose(
  filter(['dashboard.subdomain'])
)(adapter(window.localStorage));

const createPersistentStore = compose(
  persistState(storage, "someKey")
)(createStore);

const store = createPersistentStore(rootReducer, { dashborad: initialState });

これらのライブラリを使用するうえでの注意点は、redux-localstorageのversionです。1.0.0-rc4を採用しました。これは、redux-localstorage-filterを使いたかったからです。

Async

XHRでAPIサーバから稼働状況やお知らせを取得します。Fetch APIでXHRを実現したかったので、以下のライブラリを採用しました。

"dependencies": {"babel-core": "^5.8.25",
  "isomorphic-fetch": "^2.1.1",
  "redux-thunk": "^1.0.0"}

isomorphic-fetchはFetch APIのpolyfillです。

FacebookのFluxでは、action creatorsがdispatchまでやります。

対して、Reduxではaction creatorsはpure functionsで単純にactionを返すものです。これをstore.dispatchでstoreにdispatchするのがReduxのお作法になります。

Actions | Redux

Reduxでは、middlewareという、store.dispatchをWrapした関数を作れる仕組みがあります。 redux-thunkではこの仕組みを用いて、「非同期処理を行い、結果を元にaction creators関数を呼び出す」関数をstore.dispatchでWrapした関数を作ることができます。 あとは、アプリケーションの初期化時のReact Lifecycle メソッドや、ユーザインタラクションイベントハンドラ等で、作った関数を呼び出します。 実装は、Redux documents/Async Actionsを参考に、Promiseのエラー処理を少し追加しました。

Multilingualization/Localization

cybozu.com 稼働状況には、

  • 日本以外にも米国で同様のcybozu.com 稼働状況サービスを提供(今後は中国も対応予定)
  • 米国のcybozu.com 稼働状況はUIがかなり違う
  • 文言を切り替えるだけじゃなくてコンポーネントレベルで切り替える必要がある

といった要件があります。

f:id:cybozuinsideout:20151028114947p:plain
日本リージョンにおける日本語表示の cybozu.com 稼働状況
f:id:cybozuinsideout:20151028114957p:plain
米国リージョンにおける日本語表示の kintone.com 稼働状況

多言語化対応とロケールを考慮したdatetime操作のために以下のライブラリを導入しました。

"dependencies": {"i18next-client": "^1.10.2",
  "moment": "^2.10.6"}

社内事情ですが、cybozu.com 稼働状況では、日米でステークホルダーが異なります。例えば、米国の担当者から 「フッターのこの部分を米国では変えたいんだけど。」 といった要望が来ることがあります。これに柔軟に対応するために、日米間で異なるComponentを提供し、それぞれのComponentをWrapしたComponentにリージョン情報を渡すことでレンダリングするComponentを変えるように実装しました。米国の画面修正対応が日本の画面に影響を与えないようになるので、Component単位でのTestが楽になります。

JP,US,CNでFooterを出しわけるコードはこのような感じになります。

class RegionalComponent extends Component {
  render() {const{ regions } = this.props;
    const region = getRegion();
    const el = regions[region];
    invariant(el, 'Component for %s is required.', region);
    return el;
  }}

RegionalComponent.propTypes = {// regionsに各リージョンごとのComponentを受け取る
  regions: PropTypes.shape({
    JP: PropTypes.element.isRequired,
    US: PropTypes.element.isRequired,
    CN: PropTypes.element.isRequired
  }).isRequired
};

class JPFooter extends Component {
  render() {return (
      // blah
    );
  }}// US,CNも同様exportdefaultclass Footer extends Component {
  render() {const regions = {
      JP: <JPFooter/>,
      US: <USFooter/>,
      CN: <CNFooter/>
    };
    return (
      <RegionalComponent regions={regions} />
    );
  }}

ES6

Reactでは、babelを使ったES6開発が当たり前になっていて、Reactを勉強すると、ES6の勉強にもなります。 今回のプロジェクトでよく使ったES6 syntaxは以下のようになります。

  • アロー関数(arrow function)
  • クラスと継承(class, extends)
  • let/const
  • 関数のデフォルトパラメータ(default parameter)
  • スプレッドオペレータ(spread operator)
  • 分割代入(destructuring)

特に、propsや(store.dispatchでWrapした)actionをRoot Componentから下位Componentに渡していく際に、少ない記述で表現できる、スプレッドオペレータ・分割代入にはとてもお世話になりました。

class App extends Component {
  render() {// destructuringconst{ announcements, inputSubdomain, status} = this.props.dashboard;
    const props = {
      announcements,
      inputSubdomain,
      status};
    return (
      {/* spread operator */}<Contents {...props} />
    );
  }}

Utility

React/Redux開発で、stateやactionのpayload情報を追いかけたいときは、redux-loggerが便利です。開発モードフラグを設定すると、 actionの前後のstateやactionに載っているpayloadの情報をBrowser consoleに出力してくれます。

f:id:cybozuinsideout:20151028215326p:plain
redux-logger

また、Reactでは関数型プログラミングが推奨されています。Actionによるstateの変更時には、

をオブジェクトのコピーに使いました。

exportdefaultfunction dashboardReducer(state = {}, action) {switch (action.type) {case ActionTypes.CHANGE_INPUT_SUBDOMAIN:
    const inputSubdomain = action.payload.subdomain.trim();
    returnObject.assign({}, state, {
      inputSubdomain
    });
  default:
    return state;
  }}

Lint

チーム開発をやる上で、主にレビュー工数削減のために、

  • coding styleを統一したい
  • 静的解析で見つけられるBugやcode smellは取り除きたい

ということがありました。また、天野の経験からもあとから静的解析ツールを入れることは初期時の導入に比べてコストがかかるということがあったため、開発開始段階からESLintを導入しました。

"devDependencies": {"eslint": "^1.5.1",
  "eslint-plugin-react": "^3.4.2"}

最初から最もきついRuleで運用し、Ruleの適用方法を変えたくなったら都度相談するという運用でやりました。

ESLintは先人の知恵がRuleに詰まっていて、

  • JavaScriptになれない開発者がつまづきそうなポイントが抑えられている
  • 関数で受け取った変数に副作用がないような書き方もある程度強制できる
  • ES6の書き方も強制できる

といった部分が魅力的でした。

例えば、reducerで前のstateを変更するようなコードがあるとします。

function someReducer(state = initialState, action) {switch (action.type) {case ActionTypes.INIT:
    // 引数のstateを変更
    state.foo = 'reassigned';
    return state;
  default:
    return state;
  }}

.eslintrc に次のような設定を書きます。

{"rules": {"no-param-reassign": 2
  }}

対象ソースにESLintにかけるとRule Disallow Reassignment of Function Parameters (no-param-reassign)に、次のように怒られます。

$ eslint path/to/src --config path/to/.eslintrc
/path/to/somereducer.js
  10:5  error  Assignment to function parameter 'state'  no-param-reassign

✖ 1 problem (1 error, 0 warnings)

ESLint Rules

eslint-plugin-reactを入れるとReact関連のLintもできます。

nodejsのLintにも使われているようです。

開発当初(9月下旬)は、npm@3で運用していたのですが、ESLint関連の依存解決がうまくいかなかったので、npm@2で運用しています。

Testing

テストにも、ES6を採用しました。主なライブラリの構成は以下のようになりました。

"devDependencies": {"espower-babel": "^3.3.0",
  "jsdom": "^6.5.1",
  "mocha": "^2.3.3",
  "mocha-jsdom": "^1.0.0",
  "power-assert": "^1.0.1"}

reducerのTestはこんな感じで書きます。

describe('dashboard reducer', () => {
  jsdomReact();
  
  it('サブドメインが変更されたら、変更されたサブドメインを返す', () => {const subdomain = 'yusya';
    const action = {
      type: ActionTypes.CHANGED_INPUT_SUBDOMAIN,
      payload: { subdomain }};
    
    const actual = someReducer(initialState, action);
    
    assert(actual.inputSubdomain === 'not-yusya');
  });
});

これをmochaで動かすと次のようなテスト失敗結果が得られます。

$ mocha --compilers js:espower-babel/guess test/**/*.js

  1) dashboard reducer サブドメインが変更されたら、変更されたサブドメインを返す:

      AssertionError:   # test/reducers/dashboard.spec.js:24

  assert(actual.inputSubdomain === 'not-yusya')
         ||||"yusya"false
         Object{announcements:#Array#,changed:true,inputSubdomain:"yusya",notFound:false,status:#Object#,subdomain:""}

  --- [string] 'not-yusya'
  +++ [string] actual.inputSubdomain
  @@ -1,8+1,4 @@
  -not-
   yusy


      + expected - actual

      -false
      +true

      at decoratedAssert (node_modules/power-assert/node_modules/empower/lib/decorate.js:42:30)
      at powerAssert (node_modules/power-assert/node_modules/empower/index.js:58:32)
      at Context.<anonymous> (test/reducers/dashboard.spec.js:24:5)

Sales Systemチームでは、サーバサイドのJavaのTestで、Spockを使っているため、power-assert

  • "No API is the best API."の思想
  • わかりやすいレポート

などexpectにはないメリットが個人的には良かったです。

また、power-assert + ES6 でTestを書く上で、espower-babelのおかげで動かすまでが楽でした。

取り組んでみた感想

今回は作り直しということで技術選定に特にしがらみがなく、解決したい問題に対して使いたいものを使えたことは良かったです。

kintoneチームでも天野が主導して一部にReactを導入していますが、Google Closure Libraryとどのように組み合わせると効果的かという部分に試行錯誤しているようです。

そんな中で、小さいプロジェクトではありますが、React/Reduxの導入事例を社内に作れたのは良かったと思っています。 React/Fluxで実装すると、DOM操作をする部分が全くなくなって、どこに何を書いているのかが見通しがよくなるなということを実感できました。 また、React/Fluxではドキュメント等に、「どこに何を書くべきか?」といった指針が豊富に示されていて、チームでコードレビューをする際も議論がしやすかったです。

しいて不安な部分を挙げるとすれば、React/Reduxをもっと大きなプロジェクトに採用する際は、Storeのstateの構成がかなり大きい一枚のJSONになることが予想され、うまくそれを管理できるかが課題になりそうです。

まとめ

長くなってしまいましたが、cybozu.com 稼働状況を React/Redux で作り直した話を技術要素を中心に述べました。

また、課題となっていた、「インフラチームがクリティカルなインフラ業務と並行して、ビジネスサイドからのUI改善要望に対応できない問題」に関しては、UIとAPIサーバに分け、文言等をフロントで持つように実装することで、画面変更への対応にインフラ側への影響なく、デプロイできるようになりました。また、インフラチームの負担が減った分アプリチームの負担は増えましたが、適切なチームが適切な部分を担当することにより、会社全体としての対応コストは下がるのではないかと期待しています。

おわりに

Cybozu, Incでは、Reactでフロントエンド開発やりたいひとを募集しています。

cybozu.co.jp

#     #                                                                      ### #  #  # ######      ##   #####  ######    #    # # #####  # #    #  ####     ### #  #  # #          #  #  #    # #         #    # # #    # # ##   # #    #    ### #  #  # #####     #    # #    # #####     ###### # #    # # # #  # #          #  #  #  # #         ###### #####  #         #    # # #####  # #  # # #  ###        #  #  # #         #    # #   #  #         #    # # #   #  # #   ## #    #    ###  ## ##  ######    #    # #    # ######    #    # # #    # # #    #  ####     ### 

竹添さんを呼んでマイクロサービス社内勉強会を開催しました

$
0
0

f:id:cybozuinsideout:20151112211037j:plain

こんにちは、開発の佐藤鉄平 (@teppeis) です。

先日、社外講師としてビズリーチの竹添直樹さん (@takezoen) をお招きして、マイクロサービスをテーマにサイボウズの社内勉強会を開催しました。

どうしてこうなった?

ビズリーチさんでは毎週金曜に勉強会を行っているらしく、ひょんなことからその社外ゲスト講師として私にお声がかかり、8月のビズリーチ社内勉強会でES6勉強会をやらせてもらいました

やられたらやりかえす!ということで、今度は逆に竹添さんにサイボウズの社内勉強会のゲスト講師をオファーしたところ、こころよく引き受けていただいた、というのが開催の経緯です。

マイクロサービス、本当においしいの?

ちょうどサイボウズ社内では「マイクロサービス」がホットなキーワードになっていました。

サイボウズ初のクラウドサービス cybozu.com の立ち上げから5年が経過し、契約社数は12,000社以上に成長した一方で、当初の設計にいくつかひずみが見られるようになりました。

  • コードの肥大化
  • コード変更による影響範囲が広がり
  • サービス間の依存とデプロイ手順の複雑化
  • サービスのリリースタイミングが他サービスとの調整により制限される

などなど、大規模化したサービスに典型的な問題点が表面化してきました。

そんな背景で、今年サイボウズでは開発チームとインフラチームの合同で現行アーキテクチャを刷新するプロジェクトをスタートしました。 その一環として、世を騒がしているマイクロサービスとは何なのか? 何を解決するのか? デメリットは何か? cybozu.com にとりいれるべき点はあるか? などをちょうど議論しているところで、今回の竹添さんの勉強会が開かれました。

知見が山盛り!

竹添さんの講演内容は、こちらの発表をベースに、求人検索エンジン「スタンバイ」の開発運用経験から得られた最新の知見を加えて大幅アップデートしていただいたものでした。

  • ライブラリとサービスどちらを選択するか?
  • メッセージフォーマットに何を使うか?JSONで良い?
  • 同期?非同期?
  • APIのバージョニングどうする?
  • トランザクションどうする?
  • 組織、チームはどういう構成で開発する?
  • Scalaやりましょう!
  • 性能どうなの?
  • 障害起きたらどうなるの?
  • モニタリングどうするの?
  • 結局やってよかった?
  • Scalaやりましょう!

などなど、論点になりがちなポイントについて、具体的かつ参考になる様々な知見を共有していただきました。 特に技術面だけでなく組織面についても多く触れられていて、そういう経験しなければ分からない部分についてお聞きできたのはありがたかったです。

講演終了後の質疑でも、マイクの前に行列ができるほど盛り上がりました。

最後は竹添さんに「We are hiring!」をお返しされましたが、サイボウズでも積極採用しておりますので何卒よろしくお願いします!


クローズドな勉強会だとある程度踏み込んだ生々しい話もできるので、パブリックな勉強会とは違った良さがありますね。 このような勉強会での交流にご興味のある企業さん、ぜひ @teppeisまでお声がけください。

JavaScript大規模開発を語りに行ってきた「KOF2015」参加レポート

$
0
0

どうも、大阪開発部のケノドンです。

11月6日~7日に大阪で開催された「関西オープンフォーラム(KOF)2015」にブース出展しました。今回も、ソフトウェア、ハードウエア、ロボット、OS、データベース等、様々なものが展示されていました。

f:id:cybozuinsideout:20151112181231j:plain:w250f:id:cybozuinsideout:20151112181237j:plain:w250

私達のブースは、大きく3つのテーマに分かれていました。

  1. 大阪オフィス移転
  2. 大規模フロントエンド開発について
  3. 悟空ボウズマンを助ける体験

大阪オフィス移転

11月2日から梅田阪急ビル オフィスタワーに移転しました。
近くに居らっしゃったら是非お越しくださいませ!

cybozu.co.jp

大規模フロントエンド開発について

大規模Webアプリケーション「kintone」のフロントエンド開発で利用しているツールや言語について語りました。

JavaScriptによる開発

JavaScriptの開発に当って、次のような課題が考えられます。

  • スクリプト言語のため、不具合やコードの異常が、コードを実行するまでわからない
  • 動的型付けなので、エンジニアがコードから型を読み取らなければならない
  • 書き方の自由度が高くて、既存のコードの調査や修正、影響範囲の調査などで、コードを読む時のコストが上がる
  • DRYが守られない
  • ブラウザが読み込んで実行する事で読み込むコード量が増えるとダウンロードに時間がかかってしまう

現在の kintone の JavaScript は30万行を超えており、保守にかかるコストが開発リソースを圧迫してもおかしくない程の規模になっています。

Google Closure Compiler

コードの確認

JSDoc で引数や関数の返り値の型の定義をする事によって、Closure Compiler はコードに不整合があることを検知してくれます。

/** * @param {boolean} isBoolean isBooleanの引数が boolean である事を Compiler に教えておく */function logBoolean(isBoolean) {if (isBoolean) {
    console.log('It is true.');
  }else{
    console.log('It is false.');
  }};

logBoolean(false);    // It is false.
logBoolean('false');  // 'false'は boolean ではないため、COMPILE ERROR!

同じように、変数に与えられる値、関数の使い方などを確認します。そのためはちゃんとJSDocを書かなければなりません。他にシンタックスエラー、関数に渡す引数の数、定義されていないプロパティなど、いろいろみてくれます。
このような Closure Compiler による事前解析によって、単純な不具合を防ぐ事が想像できると思います。

コードの圧縮

先ほどの例をもう一度使います。

/** * @param {boolean} isBoolean */function logBoolean(isBoolean) {if (isBoolean) {
    console.log('It is true.');
  }else{
    console.log('It is false.');
  }};

logBoolean(false);
logBoolean(true);

このコードは250byteぐらいあります。Closure Compiler が最適化をしてくれたら下記のようになります。

function a(b){b?console.log('It is true.'):console.log('It is false.')}a(!1);a(!0);

なんと、100byte以下になりました。
圧縮する事で、ブラウザ上の読み込みが速くなって、ユーザにより良いサービスを提供ができます。

Google Closure Library

Closure Tools の一つであり、包括的な JavaScript ライブラリです。

  • ライブラリでできることはライブラリに任せる
  • DOM管理が快適
  • Closure Compiler と相性が良い

ライブラリを使いこなすことで効率的かつ統一的なコードがかけます。

例えば、少し修正した体験アプリのクラス kintone.component.Button をご覧ください。

/** * ボタンのコンストラクタ * * @param {goog.dom.DomHelper=} opt_domHelper * @constructor * @extends {goog.ui.Component} Closure Library のクラスを継承している */
kintone.component.Button = function(anyBoolean, opt_domHelper) {// 親クラスのコンストラクタを呼び出す。
  kintone.component.Button.base(this, 'constructor', opt_domHelper);
};
goog.inherits(kintone.component.Button, goog.ui.Component);

/** * 当クラスの DOM が生成するに呼ばれる関数 *  * 親クラスの goog.ui.Component に定義されているので override アノテーションを設定 * @override */
kintone.component.Button.prototype.createDom = function() {// 次の Closure Template に定義されている kintone.template.soy.buttonDiv のテンプレートから DOM を生成するvar el = goog.soy.renderAsElement(kintone.template.soy.buttonDiv, null, null, this.getDomHelper());
  // 当インスタンスのエレメントを設定する。生成した DOM と JavaScript オブジェクトの連携ができた。this.setElementInternal(el);
};

/** * document に DOM が入れられた時に呼ばれる関数 *  * ボタンにクリックされる事をListenする * * @override */
kintone.component.Button.prototype.enterDocument = function() {
  kintone.component.Button.base(this, 'enterDocument');
  // インスタンスの DOM の第一子エレメントを取得する。var callButton = goog.dom.getFirstElementChild(this.getContentElement());
  // クリックされた時に this.callBozuman を呼び出すように設定する。this.getHandler().listen(callButton, goog.events.EventType.CLICK, this.callBozman);
};

/** * 悟空ボウズマンの画像インスタンスを作成して表示する */
kintone.component.Button.prototype.callBozuman = function() {// 悟空ボウズマンインスタンスを作成するvar goku = new kintone.component.Goku();
  // 悟空ボウズマンを document に入れる
  goku.render();

  // 当ボタン自体はdocumentから破壊するthis.disposeInternal();
};

上記の様に、簡単に DOM の生成、管理ができます。

Google Closure Template

動的なUI生成システムです。

  • HTMLやUIパーツを再利用可能な塊として扱える
  • Java と JavaScript から利用できる
  • パーツ自体はHTMLを拡張したような形で、書きやすく読みやすい

再利用によって、同じような異なるコードを防ぐことができます。

上記に呼び出された kintone.template.soy.buttonDiv テンプレートの中身をみてみましょう。

{namespace kintone.template.soy}

/**
 * 悟空ボウズマンを呼び出すボタン
 */
{template .buttonDiv}
  <div class="content-wrapper">
    <a href="#" class="button">ボウズマンを助ける</a>
  </div>
{/template}

HTMLをパーツとして命名でき、パラメータも設定可能で、共通化しやすいのでとても便利です。

Google Closure Linter

JavaScript のファイルをチェックしてくれます。

  • セミコロンはある?
  • 規約と異なる書き方されてない?
  • JsDoc のアノテーションが正しく書けてる?

人による細かいミスを事前に防ぐことができます。

まとめ

  • 静的型付 + 圧縮 + 最適化したいから Closure Compiler
  • Closure Compiler と一緒に利用するため Closure Library
  • Closure Library と相性の良い Closure Template で DOM生成
  • コード規約を仕組みで縛るために Closure Linter

CSSの管理について

CSSでよく現れる問題

  • マジックナンバー:意味のわからないpx指定などが増える
  • いろんなブラウザで同じようなスタイルにするために、大量の記述が必要なことがある
  • 再利用ができなく、冗長したスタイルが増える

という事で、大規模にしてメンテナンスコストが爆発する!対策として、2つのツールを紹介します。

Sass

  • 変数や計算が利用できる ⇒ 値に意味をもたせられる
  • ネスト構造で直感的なスタイル指定 ⇒ 整理しやすい
  • コンポネント化が可能 ⇒ 再利用ができる

autoprefixer

  • 一つのスタイルからすべてのブラウザに必要なスタイルを生成してくれる

実装が楽になり、メンテナンスコストが減るのも、人が書くことによるミスを防げる事がメリットとなります。

サンプル

$great-color: #0a3c59;            // 変数を定義
%common-border {                  // 再利用のためのスタイル定義
  border: 1pxsolid $great-color; // 変数を利用
}.common-button{
  @extend %common-border;         // %common-borderを呼び出す
}.big-button{
  @extend %common-border;         // %common-borderを呼び出す
  border-radius: 6px;
  &:hover {                       // ネスティングを利用
    color: $great-color;          // 変数を利用
  }}

悟空ボウズマンを助ける体験

Google Closure Compiler が具体的にどういう風に役立つかを感じてもらうために、体験アプリを作りました。
githubに公開しているので是非ご覧ください。

f:id:cybozuinsideout:20151110163412j:plain:w400

内容

コードに問題を含んだアプリを用意しました。Closure Compiler を使って、エラーの検知をし、対応していくような体験になります。

全部で5個、エラーを一個ずつ直してみましょう。

1. パースエラー

app/javascript/goku.js:133: ERROR - Parse error. ',' expected
  var el = goog.soy.renderAsElement(kintone.template.soy.gokuMessage, null, null, this.getDomHelper();
                                                                                                     ^

原因

goog.soy.renderAsElementの締め括弧が足りないというエラーになります。

解決

)を足す。

2. 定義されていないプロパティ

  this.getHandler().listen(callButton, goog.events.EventType.CLICK, this.callBozman);
                                                                         ^

原因

すぐ下に定義されているプロパティがあります。kintone.component.Button.prototype.callBozumanは確かにありますが、良くみたら綴りに差が出ています。uが不足しています。

解決

this.callBozmanuを足す。

3. 引数の数エラー

app/javascript/goku.js:112: ERROR - Function goog.ui.Component.prototype.getContentElement: called with 1 argument(s). Function requires at least 0 argument(s) and no more than 0 argument(s).
        gokuMessage.render(this.getContentElement(true));
                           ^

原因

エラーの内容は this.getContentElementに引数を一つ渡しているけれど、この関数は引数が必要ないということが書いてあります。

解決

引数として渡している trueを消す。

4. 引数型エラー

app/javascript/setup.js:15: ERROR - actual parameter 1 of kintone.component.Button does not match formal parameter
found   : string
required: boolean
  var button = new kintone.component.Button(kintone.setup.anyBoolean());
                                            ^

原因

エラーの内容を見ると、 kintone.component.Buttonbooleanの引数を求めていますが、stringが渡されています。Compiler がなぜそう判断したかというと
* kintone.component.Buttonの Doc に @param {boolean} anyBoolean 意味のないbooleanがある事で、Compiler は booleanを渡す必要性がわかる * kintone.setup.anyBooleanの中身は return true;と書いてあるにも関わらず、 Doc が @return {string}となっているので、Compiler は kintone.setup.anyBooleanstringを返すと判断する よって、kintone.setup.anyBooleanの名前とその内容から、Doc に問題があるとわかります。

解決

kintone.setup.anyBooleanの Doc に返り値の型を booleanに書き換える。

5. 返り値型エラー

app/javascript/setup.js:27: ERROR - inconsistent return type
found   : boolean
required: string
        return true;
               ^

原因

kintone.setup.anyBooleanは Doc に @return {string}が書いてあるのに return true;をしている事で、問題があると Compiler が判断しました。 実は一つ上のエラーを修正で、このエラーも直されました。

解決

kintone.setup.anyBooleanの Doc に返り値の型を booleanに書き換える。

まとめ

スクリプト言語である JavaScript でも Google Closure Tools を利用することで、事前にいろいろのような問題の検知ができる事がわかりました。kintone のように、規模が大きくなるととても大切な仕組みとなってきます。

最後に

二日間だけでしたがいろんな人と出会えてとてもよかったと思います。また、ブースに来てくださった皆様、アドバイスや応援のメッセージをくださった皆様にもありがとうございます!
来年のKOFに向かって皆様に楽しんで頂けるネタを考えて行きたいと思います!

サイボウズでエンジニアとして働く事の魅力を探る会を開催しました

$
0
0

f:id:cybozuinsideout:20151116110347j:plain

こんにちは!東京第2開発部の田中(@yuichielectric)です。

先日、サイボウズでエンジニアとして働くことの魅力はどこにあるのかを探る「ここが良いよね、サイボウズのエンジニア」ワークショップを社内で開催しました。

希望者のみの参加で、業後に2時間の開催ながら35名(エンジニア以外の職種からも8名)が参加し、想像以上に盛り上がり、自分たちの職場の魅力はどこにあるのかを再発見する良い機会となりました。

なぜやったのか

この会の発端は、エンジニア採用チームで2016年度新卒採用活動の振り返りを行っていた時に出た思いつきでした。

エンジニア採用チームは、その名の通りエンジニアの採用活動の企画を行うチームです。メンバーは、人事部のメンバーが2名、東京・大阪・松山のエンジニアマネージャ4名の計6名で構成されています。

このエンジニア採用チーム内で2016年度新卒採用活動の振り返りを行っていて、世の中に多数の魅力的なIT企業がある中でサイボウズにしかない魅力をどのように伝えるのか、という課題が出ました。

この課題に対して、じゃあそもそも今サイボウズで働いているエンジニアはどこに魅力を感じているのか、またエンジニア以外の職種の人から見て、サイボウズのエンジニアに対してどんなイメージを持っているのかワークショップをしてみよう!という話になり、今回のワークショップを企画する事になりました。

事前のアイデア出し

ワークショップ当日にいきなり参加者にサイボウズでエンジニアとして働くことの魅力を書き出してくださいと言っても、なかなかアイデアが出ない可能性もあります。そういったケースを防ぐために、事前にアイデアを登録するためのkintoneアプリを用意しました。kintoneはこのような皆にデータを登録してもらいたいケースに非常に簡単にアプリを作る事ができるので便利ですね!

f:id:cybozuinsideout:20151117105312p:plain

事前にアイデアを登録するアプリを用意した結果、当日までに48件ものアイデアが集まりました。事前に全く何も考えずに参加するメンバーが多少は少なくなると良いかなと思って用意したのですが、思いの外たくさんのアイデアが登録されて、このアプリ上での議論も行われたりと十分な効果を上げることができたと思います。

f:id:cybozuinsideout:20151116115006p:plain

当日の流れ

2016年新卒採用活動の総括

まず、いきなりワークショップを始めるのではなく、参加者全員に今のサイボウズの採用の情報を背景情報と理解してもらうため、人事のメンバーから2016年新卒採用活動の総括と振り返り結果の発表を行いました。

f:id:cybozuinsideout:20151117155122j:plain

2016年採用活動での会社紹介スライドの紹介

その後、今のエンジニア採用活動で使っている会社紹介を、今回のワークショップの参加者向けに行いました。

f:id:cybozuinsideout:20151116130442j:plain

現状行っている会社紹介の内容を紹介することで、ここから更に改善するにはどうしたら良いのだろうと考えるベースとしてもらう効果を狙っています。

アイデア出し

その後、5〜6名のグループに分かれて、ホワイトボード、ポストイット、マーカーを使って、サイボウズでエンジニアとして働くことの魅力の洗い出しを行いました。

f:id:cybozuinsideout:20151116170316j:plain

ここでは、まず個人毎にどんどんアイデアをポストイットに書いていってもらいます。その後、各人が考えたアイデアをグループ内で共有してもらい、似たアイデアをグルーピングしてホワイトボードに貼っていきます。

f:id:cybozuinsideout:20151116170507j:plain

課題発表

ここで、各グループに「採用活動の発表資料にもう1枚、サイボウズでエンジニアとして働くことの魅力を伝えるスライドを追加したいと考えています。そのスライドをグループで作ってください」というお題を発表します。各グループ内でこれまで出てきたアイデアを元に、何をどうエンジニア志望の学生に伝えるべきかを考えてもらいます。

f:id:cybozuinsideout:20151116171057j:plain

最終発表

そして、最後に各グループの発表を行いました。

f:id:cybozuinsideout:20151116172333j:plain

各チームの発表が終わった後に、参加者全員に自分がもっとも共感した発表を投票してもらいました。

もっとも共感を集めたのは

その中で、もっとも共感を集めた(35名中20名以上の共感を集めた)発表はこちらです。

f:id:cybozuinsideout:20151117102044p:plain

タイトルは「開発文化をつくる文化」です。

このタイトルには、サイボウズのエンジニアの開発文化はトップダウンに決まるものではなく、現場のエンジニアが自ら作り出しているのだというメッセージが込められています。

その背景として「営業と開発の信頼関係」、「クラウド開発」、「コミュニケーションツール」が挙げられています。

営業と開発の信頼関係

サイボウズでは、多くのエンジニアが営業と開発の間で信頼関係があると考えています。そのため、営業が要求するものを開発が粛々と実装するという開発の進め方に終始するのではなく、開発側から提案を行って機能を実装したり、自らの開発プロセスやサービスの品質向上の活動に時間を割くことができています。

クラウド開発

サイボウズでは、cybozu.comというクラウドサービスを提供していますが、内部的には複数の内部サービスに分かれて、それぞれ別のチームで開発をする体制になっています。

そのため、チーム間のコミュニケーションを密に行う必要があり、業務上のコミュニケーションだけでなく、共同で勉強会を行ったりもしています。

コミュニケーションツール

サイボウズは、社内での情報共有やチームワークを促進するためのコミュニケーションツールを開発しています。それと同時に、サイボウズ社内でも自社のサービスを非常にヘビーに使っています。そのため、部署やチームをまたいだコミュニケーションや情報共有が非常に活発に行われています。

開発文化をつくる文化

このような背景の結果、スクラム開発の導入やKAIZEN活動(KAIZEN活動KAIZEN合宿)、チーム横断の勉強会などといった、現場のエンジニアが自主的に始めて定着していった取り組みが数多く産まれました。

また、自社で開発しているコミュニケーションツールによってチームをまたいだコミュニケーションを活発に行うことができるので、あるチームで定着した良い取り組みは他のチームにも広がっていったり、営業などの他の部署もその活動の成果に対してフィードバックをくれるということが頻繁に発生します。その結果、良い取り組みはサイボウズの組織全体の文化として定着していきます。そして、そのより改善された文化のもとで更に良いコミュニケーションツールを開発していき、その結果更に良い開発文化が作り上げられるという、ポジティブなフィードバック・ループが形成されています。

このようにして、自分たちの文化や自分たちの仕事のやり方は自分たちで作り上げて改善していくという文化が根付いています。

こういった文化の中で働くことが出来る点が、サイボウズでエンジニアとして働く上での一番の魅力なのではないか、という発表でした。

この話には非常に多くの共感が寄せられ、また「開発文化をつくる文化」という言葉もサイボウズの開発文化を簡潔に表現できていると評判でした。

まとめ

今回のワークショップ自体も、日頃採用活動に関わっているメンバーが自分たちの問題を解決する上で現場のメンバーの声を聞こうと開催したもので、自分たちの文化を自分たちで作っていく良い例になったと思っています。希望者のみが参加するワークショップだったのですが、そこに35名ものメンバーが参加してくれたり、ワークショップ開催より前に用意していた事前アイデア出しアプリにも沢山の意見を登録してくれたりと、自分たちの仕事の進め方をいかに改善していくかという点に興味を持っているメンバーが非常に沢山いるということも改めて実感することが出来ました。今回のワークショップを踏まえて、今後の採用活動をよりサイボウズの魅力を伝えることができるものにしていこうと思います。

また、採用活動以外でも、今回のようなワークショップをもっと開催しよう!という声も出てきています。このように、自分たちの仕事のやり方や自分たちのサービスの改善の仕方は自ら促進していくという文化は非常に重要なものだと私は思います。今後もこの文化をより強固なものにしていけるよう活動していこうと思います!

エンジニア大募集中!

サイボウズでは、このような開発文化を持つ職場で働きたいエンジニアを東京・大阪・松山の各拠点で絶賛募集中です!

また、こんなサイボウズの開発現場を間近で見てみたい学生さん向けにインターンも募集中です!

おまけ:大阪・松山オフィスからのリモート参加

ちなみに、今回のワークショップでは、大阪オフィスと松山オフィスのメンバーも参加していました。最後に、リモートメンバーも交えてのワークショップの取り組みについても紹介したいと思います。

リモートのメンバーも、他のグループと同様に東京のメンバーと一緒にグループを作って、アイデア出しやグループ内の議論を行いました。

その際、リモート参加のメンバーが居るグループのテーブルには、テレビ会議システムを配置し、アイデア出しのフェーズではkintone上でアイデアを東京側のメンバーと共有し、東京側のメンバーが書き出してホワイトボードに貼るようにしました。

f:id:cybozuinsideout:20151116171355j:plain

また、議論のフェーズでは、ホワイトボードにテレビ会議システムのカメラを向けて、ホワイトボードを見ながら議論を行いました。

f:id:cybozuinsideout:20151116171746j:plain

こうした工夫の結果、結構快適にワークショップを進めることができたそうです。サイボウズ社内でもリモートの会議や、リモートメンバーを含めた少人数での議論は頻繁に行っているのですが、今回のようなホワイトボードを使ったワークショップでリモートメンバーを含めてやるというのは初めてだったので、これは思わぬ収穫でした。

nginx の設定をレビューするときの観点をまとめてみた

$
0
0

こんにちは。 インフラチームの野島(@nojima)です。

チームのメンバーに nginx の設定について気をつけるべき点を共有するために、レビュー観点を書きました。 せっかくなのでここで公開します。

ほとんどの項目は自分やチームのメンバーの実体験に基いています。

レビュー観点

server

  • server_nameが他のやつと被っていないか。
    • listen する IP アドレスが同じ場合、server_nameで区別できないといけない。
    • TLS を使う場合、SNI をサポートしないクライアントでは TLS 用の設定が default_serverのものが使われる点にも注意。
  • TLS を使う場合、listenディレクティブに sslオプションを書いているか。

location

  • locationのマッチの順番に注意
    • 正規表現の locationは前方一致の locationよりも優先度が高い。 意図せず別の locationを隠してしまっていないか確認する。
    • また正規表現の location 同士は上に書かれたものが優先されるので正規表現 location 同士でも注意が必要。 (前方一致の location の場合、順番は関係なくて、より長い location が一致されるため、普通は大丈夫)
  • 正規表現に注意
    • ^とか $を付けるべきか付けないべきか。
    • index.htmlじゃなくて index\.html
    • 正規表現エンジンに PCRE が使われているので、バックトラックが大量に起こりうる正規表現があると DoS をされる可能性がある。 常にバックトラックが起こらない正規表現を書くこと。
  • location /hoge/と書くと /hogeにアクセスされたときにマッチしない。
    • location = /hogeを作って return /hoge/$is_args$args;と書いておくと親切だが、実際のところここまでやってる設定は少ない。
      • ちなみに、rewrite ^/hoge$ /hoge/;のようにして internal redirect で処理してはいけない。相対リンクが壊れるので。

URL デコードに注意

  • nginx は URL (正確にはパスの部分) を勝手に URL デコードしてしまうことがある。
    • リクエストを /prefixをつけた URL にリダイレクトしようとして return 301 /prefix$uri;とやると嵌まる。
      • /hoge%3Fpiyo/prefix/hoge?piyoにリダイレクトされる。
    • $request_urireturnできないか検討すること。request_uriはデコードされていない URL が格納されている。
      • ちなみに $uriは引数の部分を含まないが $request_uriは引数の部分を含む。紛らわしい。
  • rewrite ^(.*)$ /prefix$1 redirect;なども似たような問題がある。
    • やっぱり /hoge%3Fpiyo/prefix/hoge?piyoにリダイレクトされる。
    • rewriteの場合は単純に URL デコードされるわけではなく、文字によってデコードされたりされなかったりする。
  • さらに言うと、URL デコードだけでなく、駆け上がり処理 (/hoge/../fuga/fugaにするようなやつ) とかも行われる。
    • 駆け上がり処理は URL デコードした後の文字列で行うので、/../を URL エンコードしたりしても回避できない。
  • Apache は %2FをURL デコードしないという謎の仕様があるが、nginx にはこの仕様がないので微妙に互換でない。

proxy_pass

  • proxy_passはホスト名まで書く場合とパスの部分がある場合で挙動が変わる。
    • つまり proxy_pass http://foo;と書く場合と proxy_pass http://foo/;は異なる挙動をするということ。
    • パスを指定してしまうと %2F%2Bなどの一部の文字が勝手にデコードされる問題が発生する。パスを指定しない場合はパスをそのままバックエンドに渡してくれる。
    • ということで基本的にパスは書くべきでない。proxy_pass http://foo;の形式を用いるのが安全。
    • /hoge.index/prefix/hoge.indexにリバースプロキシしたいみたいな場合はどうしてもデコードが避けられない。
    • また、proxy_passは URL に変数を含む場合と含まない場合で挙動が変わるが、マニアックなので省略。
    • さらに proxy_passを含む locationは挙動が微妙に変わる。これに関してはマニュアルを参照

フェイズに注意

  • returnrewrite, setなどは denyとか allowより先に処理される等、ディレクティブの処理順番に注意。
    • deny all;としていても同じ location に return 200 "hello";とか書くと 200 が返ってくる。
    • 処理順番はドキュメントに記載されていない場合が多いので、気になる場合は実験するかソースを読むしかない。
  • 基本的に set, rewrite, returnなどのリライト系が最初に処理され、limit_reqなどのリソース制限系が次に処理され、deny, allowなどのアクセス制限系が次に処理され、次に proxy_passなどのレスポンス生成系が処理される。ログの出力は一番最後。
    • internal redirect があると内部的にフェイズが巻き戻り、また最初から順番に処理される。

その他

  • internal redirectなのか普通の redirectなのか。
  • redirectするときにパスだけ redirectすればいいのか、引数 (?以降のやつ) を引き継ぐ必要があるのか?
    • 引き継ぐ必要がある場合、$is_args$argsを末尾に付けないといけない。忘れやすいので注意。
  • HSTS ヘッダを付けるべきか付けないべきか。付ける場合は includeSubdomainspreloadを指定するべきかしないべきか。
  • add_headerをすると、それより上のスコープで add_headerしたやつが全部消える。 なので下の階層でヘッダを追加したい場合は、上の階層で add_headerしたやつを全部また add_headerしないといけない。
    • error_pageなども同様。
  • allow, denyを複数書く場合は上から順番にマッチされていく。
    • allow all; deny 1.2.3.4;のように書いてしまうと 1.2.3.4は許可されてしまう。
  • レスポンスの Content-Type ヘッダの値は正しいか。
    • typesディレクティブで指定されていない拡張子のファイルがあるかチェック。
      • 現実的には、nginx の設定の管理者とコンテンツの管理者が異なるとチェックはかなり難しいけど。
    • Content-Type が間違っているとダウンロードされてほしいところでインライン表示になったり、インライン表示されてほしいことろでダウンロードされたりする。
      • 実際これでよく問題になる。
    • gzip_typesなど Content-Type で動作が変わるようなディレクティブもある。
    • また、charsetディレクティブで charset も指定すべき。文字化けによる XSS がありうるので。
      • 歴史的事情により文字コードが混在してたりすると辛い。
  • error_pageの中でエラーが起きないか。
    • error_pageディレクティブを使うとエラー時に internal redirect を起こせるが、internal redirect の先で更にエラーが起きた場合、後に起きたエラーのエラーコードがクライアントに返されるので注意。
      • エラー処理の中で起きたエラーを更にエラー処理する設定にもできるけど、ややこしいのであまり使うべきじゃないと思う。

DNS

  • 設定ファイル内にドメイン名をベタに書いた場合、そのドメイン名は nginx 起動時 (または reload 時) に名前解決され、TTL を無視してずっと保持される。
    • この名前解決は resolverディレクティブに指定した DNS サーバではなく、OS デフォルトの DNS サーバで行われる。(gethostbynameが使われている)
  • ドメイン名の指定に変数が指定されている場合、そのドメイン名はリクエストが来たときに名前解決され、TTL は遵守される。
    • この名前解決は resolverディレクティブで指定した DNS サーバで行われる。
  • 設定ファイルにドメイン名をベタ書きしたいけど TTL は遵守したい場合、一旦変数にドメイン名を set して、それをディレクティブの引数に指定するなどの工夫が必要。

適用手順

  • restart すべきか reload すべきか。
    • restart すべきなのは以下のような場合のみ:
      • nginx のプログラムを更新するとき。
      • 共有メモリ(SSLのセッションキャッシュとか)のサイズを変更したいとき。
      • リスニングソケットのオプション (setsockoptで弄るようなやつ) を変更したいとき。(ポートの変更とかなら restart しなくてよい)
    • これら以外の場合は reload する。
  • 複数回 graceful restart するときは、一個前の graceful restart が完全に終わっていることを確かめる。
    • psして古い master がいなくなったことを確かめればよい。
    • 一個前の graceful restart が完全に終わる前に新たな graceful restart を始めることは nginx の仕様上できない。
    • 例えば1時間掛けてでかいファイルをダウンロードしているクライアントがいる場合、その1時間に1回しか graceful restart はできない。

おわりに

nginx は嵌まりどころが結構多いですが、ちゃんと使うととても優秀な HTTP サーバです。 上手く利用して幸せな nginx ライフを送りましょう。

クラウドサービスのセキュリティ対策について講演しました

$
0
0

cybozu.com の運用や、サイボウズ社内の情報システムの運用を担当している山本泰宇です。

先日開催された cybozu.com カンファレンスにて、「正しく恐れるクラウドのセキュリティ」と題して、cybozu.com をはじめとするクラウドサービスのセキュリティ対策について講演しました。その講演資料を公開していますので、ブログでも紹介いたします。

docs.com

一般に、企業が利用する情報システムは個人向けのものと比較すると、高いレベルのセキュリティを要求されます(情シスとしての立場から言えば、要求します)。ただ、セキュリティと一口に言ってもその意味するところは非常に広いため、どのようなリスクに、どの程度対応するかを決めていくプロセスが欠かせません。

講演資料では割愛しましたが、実際にセキュリティ対策を考える上では、セキュリティリスクを列挙し分析したあと、必要な対策に優先順位をつけて実施していくことになるでしょう。cybozu.com はサービス開始からおかげさまで4年経ちましたが、大きなセキュリティ事故を起こすことなく運用してこれました。その間に優先順位の高いセキュリティ対策を順々に投入してきた結果をまとめたものが、講演資料で紹介している対策群です。

サイボウズでは、セキュリティに完璧というものはないと考えています。クラウドサービスを提供する側においてもですが、利用する側においても、従業員の行動すべてを徹底的に管理するというのは困難でしょう。完璧はないという前提で、それでもいかに被害を最小限に抑えてうまく利用していくか、その判断の一助になればと思い、資料を作成して講演いたしました。

企業でクラウドサービスのご利用を検討される皆様方におかれましては、ぜひご一読いただけますと幸いです。

今後とも cybozu.com をよろしくお願いいたします!

TCPとタイムアウトと私

$
0
0

本部長や副本部長もプログラミングを(たまに)することで有名なサイボウズの運用本部長、山本泰宇です。 有名じゃないかもしれませんが、ブログに書いたので有名になるということでご了承ください。

今回は、先日発生した yrmcds に起因する障害の原因と対策を解説します。 yrmcdsというのは、サイボウズが開発している memcached互換のキーバリューストレージです。

問題の理解のため、まず TCP 通信で、通信先の相手の障害にどう対応するか解説します。

データの送信中に相手が落ちるケース

このケースはさらに二つに分かれます。

  1. 相手の OS は生きているが、通信しているプログラムが落ちるケース
  2. 相手の OS ごと(あるいはネットワークごと)落ちるケース

1 と 2 の違いは、前者の場合 RSTパケットが返ってくるのに対して、後者ではなにも返ってこない点です。後者の場合、ack されない送信データ(unacked data)がカーネルの送信バッファにたまる状態になります。

前者のケースでは、ソケット API (send等)がエラーを返すので、プログラム上の処理は容易です。 後者の場合、対応方法はいくつか考えられます。

  1. カーネルが再送信を諦めるのを待つ

    Linux の場合再送信の上限回数は sysctl の net.ipv4.tcp_retries2で設定できます。 デフォルトでは 15 回となっており、数十分間は再送を続ける動作となります。

  2. アプリケーションレベルでタイムアウトする

    一定時間内に通信相手の応答がない場合はエラーとするようにします。 通信プロトコルを自由に設計できる場合は可能な選択肢になります。

  3. ソケットオプションでタイムアウトする

    ソケットを非同期にしていない(同期ソケット)場合は、setsockopt(SO_SNDTIMEO)でタイムアウト時間を設定できます。 ただしカーネルの送信バッファにまだ余裕がある場合は sendはすぐ返ってくるため、時間内に相手にデータが届いたかは保証できません。

    ソケットを非同期にしている場合、setsockopt(TCP_USER_TIMEOUT)が利用できる場合があります。 Linux の場合 2.6.37 以降のカーネルで利用できるようです。

ポータビリティや時間の保証という点を考慮すると、2 のアプリケーションレベルでタイムアウトするのが一番確実と言えるかと思います。

データの待機中に相手が落ちるケース

データ受信については、アプリケーションレベルでタイムアウト処理を実装することが多いと思います。 でも中には、無期限にコマンドを待つサーバー実装もあるでしょう。

そういうケースでは、アプリケーションレベルのタイムアウト処理の代わりに、TCP keepaliveという仕組みを利用することが可能です。

TCP keepalive は通信がまったくない場合(ここ重要)に、カーネルがアプリケーションの代わりに通信相手に確認パケットを送ることで、通信相手の存在を自動的に確認してくれる機能です。

Linux の場合 TCP_KEEPIDLE等のソケットオプションを調整することで、TCP keepalive のタイマーの動作を調整可能です。

修正前の yrmcds の実装

さて、懺悔の時間です。yrmcds はレプリケーションを実装しているので、yrmcds のサーバー間で通信をしています。 レプリケーション方式は非同期ではあるものの、causality (因果律)を保つため、オブジェクトをロックしている間にレプリケーション先にデータを送信するようになっています。

結論から先に言うと、レプリケーション先(スレーブ)が不意に消失したときに、yrmcds は自前でタイムアウト処理をしていませんでした。 また、レプリケーション用のソケットの送信バッファが詰まる場合に備えてアプリケーションレベルでもバッファを確保していたのですが、30 MB の固定サイズとなっており、実際の運用環境では約 15 秒でいっぱいになる状態でした。

結果、スレーブサーバーが不意に落ちたことの検出に、カーネルの再送信がタイムアウト(20分前後)するまでかかってしまい、バッファも不足していたためマスターサーバーの処理が滞留して障害になったものです。

私の勘違いは、TCP keepalive 処理を調整すれば、このような状況でもカーネルが自動検出して速やかにエラーにしてくれるだろうと思い込んでいた点です。yrmcds では TCP keepalive を 5 分程度で動作するようにしていました。実際には、TCP keepalive は再送中のパケットがある場合は動作を開始しないため、送信処理のタイムアウトには利用できないものでした。

yrmcds 1.1.5 の修正内容

yrmcds 1.1.5でこの問題を修正しました。

具体的には、レプリケーションスレーブがアプリケーションレベルでマスターに ping を飛ばし、マスターは ping が一定時間こないときにエラー処理をするようにしています。また、レプリケーション用のバッファサイズについても可変にし、障害検出にかかる時間の間データを貯めておくのに必要なサイズを確保できるようにしています。

また、レプリケーションバッファがいっぱいになったときはログに警告が出るようにもしています。

教訓

  • TCP の障害検出は、なるべくアプリケーションレベルで実装しましょう
  • 送信バッファサイズのようなパラメータは実運用に必要なサイズを確保できるよう実装しましょう

ご迷惑をおかけしたお客様には、大変申し訳ありませんでした。

Java トラブルシューティングガイド

$
0
0

こんにちは。ミドルウェア開発チームの青木(@a_o_k_i_n_g)です。将来の夢は藤岡弘の弟子になることです。

Cybozu では多くの Java アプリケーションが稼働しており、トラブルも発生します。僕はトラブル対応をすることが多く、今まで大小様々なトラブルを見てきました。その中で得られた知見を社内ドキュメントとして記していましたが、そちらを手直ししたものを本記事で公開します。Cybozu ではインフラ基盤に Ubuntu を用いているので各種ツールの紹介もすべて Ubuntu を前提にしていることをご承知ください。

すぐやること

各種データはトラブルが発生している状態で運用チームに取得してもらいましょう。鮮度が重要なデータも多いので、常日頃運用チームと手を取り合ってトラブル対応できる組織づくりをしておくべし。

モニタリングツールで該当環境のデータを確認

トラブルの原因は多種多様です。解決はともかくとして、まず何が起きているのかを把握するのがまずいちばん最初にやることです。

まずはじめに、どんな現象なのかを簡単に切り分けましょう。CPU 消費, メモリ 消費, IO 消費, ネットワーク不通, リソースリーク、スタベーション、など。 JVM のモニタリングには jstatを用います。jstatでヒープの New 領域や Old 領域の容量と使用量を確認したり、GC の回数を確認したりします。とは言っても jstatの結果を目で見るのはキツイので、事前に jstatの結果をモニタリングツールに流しておかなければなりません。

スレッドダンプの取得

スレッドダンプは大変役立つ上に簡単に取得でき、ヒープダンプと違って出力後のサイズも小さいので JVM でトラブルが起きたら必ず貰っておきましょう。

スレッドダンプは jstackで取得できます。 注意点として、スレッドダンプの取得は JVM 実行ユーザーと同一でなければいけません。root でも取得できません。

$ sudo -u<USER> jstack $PID> /tmp/jetty.stack.txt

余裕があるときは、数秒おきに何度かスレッドダンプを取ってもらうと尚良いです。なぜならスレッドダンプ1つだけ見た時、あるスレッドが処理が遅くてスレッドダンプに出てきたのか、それともたまたま処理している瞬間に遭遇したものなのか判別しにくいからです。

取得したスレッドダンプの拡張子は .txt にしておきましょう。というのも、会社にいない時のトラブル対応で、スレッドダンプをスマホで見る場合があります。拡張子が .stack のような謎拡張子だとスマホでダウンロードできなかったり開けなかったりすることがあるので、.txt にしておけば安心です。

ヒープダンプの取得

JVM のヒープを取得します。 jmapコマンドで取得可能。

$ sudo -u<USER> jmap -dump:format=b,file=/tmp/app.dump $PID

出力されるヒープダンプは数百 MB から数 GB を超えることもあるので、ダンプする際のディスクの空き容量に注意しましょう。

ヒープダンプはサイズが大きく、かつ、お客様データが含まれるため取り扱いに注意する必要がある等敷居が高く、「すぐやること」とは言えないかもしれません。とはいえトラブル発生時に取得しないと意味がないデータでもあります。まずはスタックトレースや lsof、その他モニタリングツールをざっと流し読みしてわからなかったらヒープダンプを貰う、という流れが良いでしょう。

一旦おさらいです。 jstatでモニタリング、jstackでスレッドダンプ、jmapでヒープダンプ、です。 jstat, jstack, jmapをトラブル解決のための三種の神器と言います[要出典]。

lsof の取得

lsof でリソースリークが無いか確認しましょう。こちらも jstackと同じく、簡単に取得出来てお客様データが混じらないので取り扱いが楽です。

$ lsof -p$PID> /tmp/lsof.txt

プロセスを特定する

CPU 使用率が高い、メモリを大量に使用している、大量にディスク書き込みをしている、などのトラブル時、どのプロセスがリソースを消費しているのか確認しましょう。

CPU を消費している時

topコマンドで CPU カラムでソート。デフォルトで CPU 使用率でソートされているはずですが、そうでない方は f キーを押してソートカラム選択画面に移動、上下キーで %CPU を選択して s キーでセット、q で元の画面に戻ると CPU 使用率でソートされています。

CPU を消費している場合に、やっておくべき重要なことがあります。 プロセスの特定だけでなく、スレッドの特定も行っておくべし!!

スレッドの特定の仕方。 top -Hコマンドか、topコマンドでプロセス表示後、Shift + H キーを押してスレッド表示をオンにする。CPU を消費しているスレッドを特定し、スレッド ID (top コマンドでの表示は PID カラム)をメモしておく。あとでこの topコマンドで得たスレッド ID と、スレッドダンプのネイティブスレッド ID (nid=XXX の値)を突き合わせて原因となるスレッドを特定しましょう。ただしこの技が使えるのは特定のスレッドが CPU を消費し続けている時のみです。

ちなみに topコマンドでのスレッド ID は 10 進数表記、スレッドダンプのスレッド ID は 16 進数表記なので Google 先生に "1234 to hex"とか聞いてさくっと変換できます。逆も当然できて、"0xABCD to decimal"とやると 16 進から 10 進に変換してくれます。"0b11011 to hex", "5324 to binary", "0o1234 to decimal", "0xABCD to octal"とかで 2 進数, 8 進数, 10 進数, 16 進数の相互変換可能。

CPU 使用率が高いからと言って、CPU を大量消費する不具合があると考えるのは早計です。何らかの処理がメモリを大量に消費した結果 GC スレッドがあくせく働いて CPU 使用率が高くなるということもあります。つまり JVM においてはメモリ消費は CPU の消費も促すのです。というわけで、CPU 使用率が高くなった時はモニタリングツールで GC の項目を確認し、GC なのかそれ以外なのかを切り分けする癖をつけましょう。

メモリを消費している時

CPU を消費している時と同様、topコマンドでメモリでソートしてプロセスを特定すべし。topコマンドのカラム的には %MEM を指定すれば大丈夫です。VIRT (仮想メモリ)や SHR (共有メモリ)でソートしないようにしましょう。ほとんどのケースでは注目したいのはプロセスが使ってる実メモリなので、%MEM または RES (実メモリ)でソートすべきです。

JVM は、スレッド単位でのメモリ管理はしていないので、CPU 消費時のようにメモリを消費しているスレッドを特定することはできません。

ディスクに書き込みをしている時

ディスクを消費している時は、どのファイルなのかを特定したい。たいてい /tmp にあるので、ls -lS /tmpなどで巨大なファイルが無いか確認しましょう。ファイルが見つかれば大抵プロセスも特定できますが、わからない場合は iotopなんかが使えます。iotopでのソートカラムの選択は topコマンドとは異なり、左右の矢印キーで選択します。

JVM アプリケーションが稼働しているサーバーだからといって犯人が JVM とは限りません。同居している別のプロセスが犯人ということもあるのでプロセスの特定は重要です。Cybozu の過去の例では、ImageMagick という画像変換ツールがディスクを大量消費したケースがありました。

すぐやることの次にやること

必要があればプロセスの再起動

お客様に影響が出ている場合、プロセスを再起動してメモリの状態等一旦クリアすべきですが、逆に、大丈夫な場合はできるだけ再起動せずその状態のまま残しておき、必要な情報を取得できるようにしておきましょう。必要なデータは貰ったと思っても、後から「あのデータが欲しい」と思うことはしばしばあります。

プロセスの起動オプションの取得

これは再起動しても変わらないものが多いのですぐ取得はしなくても大丈夫ですが、トラブル対応時に必要になることもあるので取得しておきましょう。ps aux | grep javaするか、jps -mで。jpsは JVM 実行ユーザーと同一ユーザーでなければなりません。

再・モニタリングツールで該当環境のデータを確認

Zabbix 等で当該環境のデータを確認しましょう。 ポイントは 2 つ。

  • 現象の確認(CPU 消費、メモリ消費、etc)
  • 障害が発動した時刻の特定

何が起きているのかを良く見極め、それから時刻を特定し、ログから分析していきます。

モニタリングツールで閲覧する際の注意点がいくつかあります。

時刻のタイムゾーンに注意すべし
基本的にグラフは横軸が時間軸になっていると思います。ただ、この時間が UTC なのか JST なのか、それともそれ以外のタイムゾーンなのかはきちんと把握しておきましょう。これを勘違いすると後述するアクセスログ等での解析時、時間で範囲を絞ったときに痛い目を見ます。

グラフの精度に注意すべし
これは Zabbix の例ですが、1 週間以上古いデータは間引かれて精度が落ちます。また、時間軸のスケールを大きくすると小さな値が正しくプロットされないことがあるので注意です。私はこれで泣きました。

トラブル解決に向けてあらゆる項目を見ましょう。CPU 使用率や JVM のレジデントメモリ使用量、物理メモリの空き容量あたりを見るのは当然として、普段見ないような項目もトラブル対応時くらい見てみましょう。たとえば JVM のメモリ関連の項目を良く見ると、ヒープとしてはまだ空きがあるのに Eden 領域が何故か自動拡張されず、GC が頻発してるというような現象が即座にわかるようになります。ちなみにこれは弊社で実際に起こった事例です。

とにもかくにも、モニタリングツールでの確認はトラブル解決の要なので、穴が空くほどに眺めるべし。眺めていればきっと天啓が舞い降りてくるぞ!

アプリケーションのログを確認

ここに書かずとも当たり前にやることではありますが、ログを確認する。

注意点が1つあります。log4j のログは、時刻順にソートされているわけではありません(きっと logback 等でも同様でしょう)。大体は時刻順ですが、システムの負荷が高い時は数百 ms 以上前後することがあります。

アクセスログで怪しいリクエストを探す

ユーザーからのリクエストが原因と思われる場合、アクセスログから怪しいリクエストを絞りましょう。参考までに、弊社のアクセスログは SQL インターフェースで提供されており、SQL で時刻やサービス、お客様、ステータスコード、処理にかかった時間などなどを元に絞り込めるようになっています。また、アクセスログと JVM が出力するログで、リクエストの紐づけができるようリクエスト ID のようなものを載せておくとよさ気です。

トラブル発生時のアーカイブを確認

当該環境のアーカイブを確認し、どのバージョンでトラブルが発生したのか断定すべし。アーカイブからソースコードへのトレーサビリティが求められます。運用チームのオペレーションログから追ったりリリーススケジュールから追っても良いですが、弊社では JVM アプリケーションがバージョンを返す API を用意しており、それを元に簡単にバージョンを特定できるようになってます。

解析方法

スレッドダンプ

スレッドダンプを取得したら、ひとまず grep tid= jetty.stack.txt | wc -l等でスレッド数の確認をしましょう。

JVM は何も使っていなくて寝ているだけならスレッド数は 20 程度です。そこにアプリケーションが使うスレッドが追加されます。他の環境や過去のデータと比較してスレッド数が怪しくないか確認すべし。普段からモニタリングツールなどで確認しておき、大体のスレッド数を把握しておくと捗ります。

仮に Jetty を使っている場合、Jetty のスレッド (qtp*** で始まるスレッド名) が入ってきます。詳しく追いたい時は Jetty のスレッドプールは org.eclipse.jetty.util.thread.QueuedThreadPoolクラスに実装があるので気になる時はここを見る。Jetty に限らずスレッドプール内の寝てるスレッドは通常 1 分程度で消えることがほとんどですが、すべて消えるのではなく数本のスレッドは残しておく実装になっていることもあります。

次に、遅延系のトラブルの場合、どこかのオブジェクトで wait していないか調べる。grep "waiting to"して同じアドレスを元に複数のスレッドがロックしてないか見るべし。スレッドプールで管理されていて寝てるだけのスレッドも出てくるので混同しないよう注意。

また、寝てるスレッドが多いことがほとんどなので、動いてるスレッドを見るために RUNNABLEで検索して追うと効率的です。スレッドの状態は 6 つほどありますが、RUNNABLEが実行中、BLOCKEDは別のスレッドによってブロックされている、WAITING, TIMED_WAITINGが寝てるスレッドということさえ覚えておけば大丈夫です。 https://docs.oracle.com/javase/jp/8/api/java/lang/Thread.State.html

jstackにはデッドロックを検出する機構があり、Java レベルのデッドロックを検出して表示してくれます。jstackの表示の一番下部に出るので、一応チェックしましょう。

ノウハウとして、スレッド名にトラブル解決のための何かしらの情報を埋め込んでいく方法があります。たとえば弊社のバックグラウンドで稼働する非同期処理アプリケーションでは、スレッド名に顧客 ID を埋め込むことでどのお客様で障害が発生したのかすぐ特定できるようになっています。情報を埋め込む際、間違ってもお客様の個人情報を埋め込まないようにしましょう(普通はやらないはずですが)。

lsof

これは普通に lessで見ましょう。ざっと見て同じような項目がずらーっと並んでなければ大丈夫です。TCP が原因っぽいとわかっている場合は grep TCPして見ても良いですが、やはり生で見るのが一番です。

または、TCP が原因っぽいなら sudo netstat -tnpすると良いでしょう。-p オプションでプロセスを表示してくれます。-p オプション使う時は sudo で。-p オプションを使っていてかつ sudo でない場合でも特にエラーは出ません。

JVM のメモリについて

JVM のメモリの使い方は大別して 4 つ。

  • Java ヒープ
  • パーマネント
  • スタック
  • C ヒープ

※Java8 からはパーマネント領域はなくなります。

これを覚えておくべし。スタックは 1 スレッドにつきたかだか 1MB 程度しか使わないので、異常なスレッド数でない限り問題は出ません。

メモリの状態を詳しく見たい時は前述の通り jstatを使います。とは言っても手動で jstat実行して数値を眺めるのは目に優しくないので、事前に jstatの項目をモニタリング基盤に流し込んでおく等をして可視化できるようにしておきましょう。手動で実行する場合、表記が若干独特なので慣れておくべし。OU は Old Used, EC は Eden Capacity のように、メモリの用途 + Capacity or Used となっていることが多いです。 http://docs.oracle.com/javase/jp/8/docs/technotes/tools/windows/jstat.html

jstatで手の届かないところは jcmdNativeTrackingMemoryで見れます。ただし JVM の起動オプションで指定する必要があるので、再起動が必要になります。 https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/tooldescr007.html

JVM に限らない話ですが、プロセスのメモリの状態を見るには /proc/$PID/下の statusmapsを見るという手もあります。

GC について。 Java 7 から外部から GC 打つことができるようになりました。jcmd $PID GC.runで発行できます。 https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/tooldescr006.html

メモリ系のシビアなケースについて調べている場合、ファイナライザが定義されているオブジェクトの破棄には GC が2回発行される必要があるというのはハマりどころかもしれません。 http://sakuramochi702.hatenablog.com/entry/2013/06/03/125052

ヒープダンプ

Memory Analyzer, JProfiler, Visual VM あたりを使って解析。

調べ方はツールによって異なりますが、Memory Analyer の Leak Suspect で出た部分を追っていけば大丈夫でしょう。ZipFileIndexというクラスのインスタンスが多数あるように見えることがありますが、これらは単にクラスパスの jar を保持しているだけだったりします。Finalizerクラスが多数ある場合でも、それらは GC されれば消えるので無視して大丈夫です。

ヒープダンプ見てもわからない場合、OQL を使いましょう。Object Query Language というもので、SQL ライクにヒープの検索ができます。Memory Analyzer 内で OQL の発行ができるので別途ツールを使う必要はありません。jhat 通して使う OQL と Memory Analyzer 通して使う OQL は異なることに注意。後者のドキュメントはこの辺にあります。でもまぁ、正直あまり出番は無いですね。いざという時のためにお道具箱に忍ばせています。 http://help.eclipse.org/mars/index.jsp?topic=%2Forg.eclipse.mat.ui.help%2Freference%2Foqlsyntax.html

メモリダンプ

メモリリークっぽい場合で、かつ C ヒープに原因がありそうな場合はメモリダンプを取得して解析すべし。メモリダンプは gcoreコマンドで取得する。当然メモリダンプにはお客様データが入っているので取り扱い注意です。

メモリダンプの解析は strings コマンドでバイナリ内の文字列を抜き出して推測するのが一番簡単です。

$ strings -a memory.dump | sort | uniq -c | sort-nr | less

さらに気合があるなら怪しげな場所を目で見るという手もありますが、バイナリを見たところで原因がわかるかというとかなり困難です。それでも見たい場合には hexdump -Cないしは od -Ax -tx1zあたりで見るべし。

メモリダンプは巨大になることがあります。なぜならメモリが怪しい時はたいてい大量消費している時なので。そんなメモリダンプをバイナリエディタで見るのは大変酷なので、splitコマンドで小分けにしましょう。

$ split -b10485760 jetty.gcore split.jetty

で、10MB ごとに小分けにされるので、途中のやつから hexdump -Cする。

メモリダンプ + strings のお話は以前僕が遭遇したケースで、下記記事に記してあるので参考になるかもしれません。 blog.cybozu.io

普段からやっておくこと

モニタリングシステムの整備

今の時代、モニタリングの重要性は今更説くまでもないですね。これが無ければトラブル解決できるものもできません。必ず整備しておきましょう。

弊社の監視・モニタリングの仕組みについては下記記事で一部公開されています。良かったらご参考に。 blog.cybozu.io

ログの整備

トラブル対応をした後に振り返ると、「ログに○○の情報を書いていれば早期解決できた」というようなことがしばしばあると思われます。どのようなログを出すべきか再度検討し、改善しましょう。不要なものは削り、必要なものを入れる。ログの鉄則です。

ツールに慣れ親しんでおく

ここまで記載したトラブルシューティングガイドですが、ここにある方法だけではトラブル解決は難しいです。ケースバイケースで原因の切り分けをしなければならないからです。その際には各種ツールに慣れ親しんでおくと捗るでしょう。日常生活やログ解析時には cat, grep, sort, uniq, tail, awk, ...。挙げたらキリが無いですね。jstack, jps, jmapあたりの Java コマンド系もローカルでひと通り触っておきましょう。パケット解析の tcpdump, プロセスの挙動を監視する strace, バイナリを眺める hexdumpあたりの学習もトラブルシューティング要員は必須項目です。いざトラブルの時に使い方がわからないとダウンタイムが長くなってしまうので、平和な時に学習しておくべし。

それでもわからない時は

トラブル対応していない人に現象を説明すると「ここ調べた?」と言ってくれて意外とそこは盲点だったりすることがしばしばあります。

1人で原因を特定できたらカッコいいですが、サーバーの向こうに困っているお客さんがいることを考えると悠長なことはしていられません。誰かに助けを求めることができる能力というのもトラブル対応をする人が持つべき資質だと思っています。

終わりに

トラブル対応に正解の道はありません。システムごとに仕組みは異なり、原因も多種多様にあるからです。トラブルは基本的に再発防止と共にあるので、一度改修したら次出会うのはまた別のトラブルです。今まで多種多様なトラブルを見てきましたが、振り返ってみればいずれもケースバイケースで調査していました。それでも、Java アプリケーションに限って言えば、ある程度のトラブルシューティングのパターンが出てきます。本記事がトラブル対策時の 1 つの助けになればと思います。

トラブルというものは起きてはならないものなので、それと向き合う時はつらい気持ちになったり申し訳ない気持ちになったりします。でも、落ち着いて調査しましょう。申し訳ない気持ちになったところで障害は直らないですし、人間が書くコードなのでバグが無いというのもありえないのです。

解決できなかったトラブルもあります。そういうものには結構苛まされて、夢に出てきたこともありました。でも一方で、複雑怪奇なトラブルを解明した時の喜びもまた強烈なものがあります。様々な知識を動員して原因を特定していく様は推理小説で犯人を追い詰めるかの如くであり、また、宝探しの如くでもあります。原因を特定できたらヒーローになれることもあります。トラブル対応はやりたがる人があまり多くないかもしれませんが、毎回未知の問題に立ち向かうというのは、結構、チャレンジングで面白いものですよ。

そして何より、お客様のためにつながります。サーバーの向こうで困ったユーザーを助けられるのは僕達プログラマをおいて他にいません。たとえ第三者ライブラリが原因であろうと、外部ネットワーク上の問題であろうと、僕達が作ったサービスにトラブルが起きたらそれを解決するのは僕達であり、解決を諦めてはなりません。何か異常が起きるたびにアプリケーションを再起動するのも立派な 1 つの手段ではありますが、さらなる安定稼働のために、より深く探求してみませんか。


Selenium GridをGoogle Cloud Platform上で運用した知見をまとめてみた

$
0
0

こんにちは。生産性向上チームの宮田(@miyajan)です。モンハンもう飽きたな、と思いつつ最新作を買ってしまうのが最近の悩みです。

この記事は、Selenium/Appiumアドベントカレンダー2015の8日目の記事です。もう途切れてしまったようですが、気にせずに盛り上げていきましょう!

今回は、Selenium GridをGoogle Cloud Platform上で運用してみた知見を書きます。過去の記事でも少し書いたのですが、今回は実際に運用してみた体験に基づいています。

モチベーション

Selenium Gridは、Seleniumテスト用のブラウザ環境を複数管理するためのツールです。主に、Seleniumテストを並列実行するときに使われます。弊社のkintoneチームでは、過去の記事に書いたように、Dockerを使ってSelenium Gridを構築しています。

Dockerを使っても、数十並列でテストを動かすための大規模なSelenium Gridを構築するとなると、かなりのマシンリソース(主にCPUとメモリ)が必要になります。

また、並行開発のときなど、複数のブランチで同時にSeleniumテストを走らせられるように、一時的に必要なマシンリソースが増えることもあります。

社内で必要なマシンを調達・管理していくのはなかなか大変かつ柔軟性に欠けるので、IaaSを利用してクラウド上にブラウザ環境を用意することにしました。

運用

もろもろ調査した結果、Google Cloud Platformを使い、GCE(Google Compute Engine)のVMインスタンスでSelenium Gridを構築することにしました。全体の流れとしては、Seleniumテストを実行するCIジョブの中で以下を行います。

  • Selenium Gridを構築するインスタンスの作成
  • 構築されたSelenium Gridを使ってSeleniumテストを実行
  • インスタンスを削除

上記を実現するために、必要な作業を説明します。

スナップショットの作成

Selenium Grid用インスタンスを作成するために、元となるディスクのスナップショットを作成します。

そのために、スナップショット作成のためのインスタンスを作成し、環境構築を行います。手順は以下のような感じです。

  • インスタンスの作成
    • 作り方はクイックスタートなどをご参考に
    • このインスタンスは本運用に使うわけではないですが、動作確認を考えると、マシンタイプはn1-standard-2くらいはリソースがあった方がいいです
    • この記事では、OSはUbuntu 14.04を使用しています(単に慣れてるから)
  • sshでログイン
  • Dockerのインストール
  • Docker Composeのインストール
  • docker-compose.ymlを作成
    • 以下のような感じです
hub:
  image: selenium/hub:2.48.2
  ports:
    - "4444:4444"
node:
  image: selenium/node-chrome-debug:2.48.2
  links:
    - hub
  ports:
    - "5900"

これでSelenium Gridが構築できるようになったはずなので、動作確認してみます。docker-compose.ymlを置いたディレクトリで以下を実行してください。

$ docker-compose up -d
$ docker-compose scale node=2

上記を実行後、http://(インスタンスの外部IP):4444/grid/consoleにアクセスしてください。Selenium Gridのコンソールが表示され、ノードが2つ存在していれば設定に問題ありません。

問題なさそうであればインスタンスを停止し、スナップショットを作成します。完了したら、インスタンスは削除しても大丈夫です。

Selenium Grid用インスタンス構築をスクリプト化する

ここまでの操作はコンソールからで問題なかったのですが、CIからGCEを操作するとなるとそういうわけにはいきません。

CIからGCEを利用するために、サービスアカウントを使っています。これは、ユーザーに紐付かない、アプリケーションから利用するためのアカウントです。

サービスアカウントはコンソールから作成でき、Google APIクライアントライブラリから利用するためのjsonファイルをダウンロードできます。コマンドラインツールのgcloudからは、次のように使って認証を有効にすることができます。

$ gcloud auth activate-service-account --key-file service-account.json

コマンドラインだけでなく、pythonのクライアントライブラリでもサービスアカウントを利用できました。他の言語のライブラリでは確認していませんが、おそらく利用できるものと思われます。

認証ができれば、あとはスナップショットからインスタンスを作成するだけです。gcloud computeを使った例を書いておきます。

# スナップショットからディスク作成
$ gcloud compute --project [PROJECT NAME] disks create [DISK NAME] --size "10" --zone "asia-east1-b" --source-snapshot [SNAPSHOT NAME]
# インスタンス作成
$ gcloud compute --project [PROJECT NAME] instances create [INSTANCE NAME] --zone "asia-east1-b" --machine-type [MACHINE TYPE] --metadata-from-file "startup-script=[STARTUP SCRIPT]" --scopes "https://www.googleapis.com/auth/devstorage.read_only","https://www.googleapis.com/auth/logging.write" --disk "name=[DISK NAME]","mode=rw","boot=yes","auto-delete=yes"

上記の[STARTUP SCRIPT]には、インスタンス起動時に実行されるスクリプトファイルを指定します。Selenium Gridを起動する場合、スクリプトの中身は下記のような感じになります。

#!/bin/bash

# 念のためにコンテナをすべて消しておく
docker rm $(docker ps -aq)

# docker-compose.ymlのあるディレクトリへ移動
cd /path/to/directory

docker-compose up -d
docker-compose scale node=[必要なノード数]

これで、インスタンスが生成されます。インスタンスの生成からSelenium Gridの起動までには、だいたい1分前後です。

そして、Seleniumテストで使用するためにSelenium GridのhubのIPを取得します。以下はgcloudとjqコマンドを使った例です。

$ gcloud compute --project [PROJECT NAME] instances list [INSTANCE NAME] --zone "asia-east1-b" --format=json | jq -r '.[].networkInterfaces[].accessConfigs[].natIP'

Seleniumテストが終わったら、インスタンスの削除を忘れずに行いましょう。

$ yes | gcloud compute --project [PROJECT NAME] instances delete [INSTANCE NAME] --zone "asia-east1-b"

このときに注意が必要なのは、削除が実行されないとインスタンスが起動し続け、余計な料金がかかってしまうということです。途中でジョブが失敗したときなどにも必ず削除が実行されるように設定しましょう。

コスト

費用

参考になるか分かりませんが、11月に実行したSeleniumテストケースの数と、実際の請求額について書いてみます。

現状、社内でGCE上のSelenium Gridを使っているのは1チームのみです。そのチームでSeleniumテストを実行するジョブは、11月は約300回実行されました。1営業日平均15回ですね。

そして、ジョブ1回ごとに実行されるSeleniumテストの数はおよそ1,000テストケースです。なので、11月に実行されたSeleniumテストケースの合計は、300×1,000=300,000なので、30万テストケースになります。

これに対して、11月のGCEの請求額は$200ほどでした。ざっくり計算すると、200÷300,000≒0.00067なので、1テストケースにつき$0.00067、日本円にして約0.083円となります。現状、インスタンスのリソースをかなり余らせているので、まだ費用削減できる余地はあります。

費用の構成要素は、次のような感じです。

  • インスタンス
    • CPUとメモリ数、起動時間で決まります
    • 費用の9割以上を占めます
  • ネットワーク
    • インターネット→GCE方向には課金されず、GCE→インターネット方向のみ課金なので、Seleniumテストに使う分には大きな金額を課金されることはありません
    • vncでつないで録画とかしてると転送量が大きくなる可能性があるので注意が必要です
  • ディスク
    • Selenium Gridの用途だとほぼ課金されません
    • SSDも選択できますが、Seleniumテストの実行時間には影響を与えないので通常のディスクで十分です

予算管理

GCE最大の残念な点として、予算の上限が設定できないことが挙げられます。

BigQueryのように気づいたら150万円溶かしていたということはないですが、間違えてインスタンスを起動しっぱなしにしてしまうと数万円くらいはかかってしまうので(体験済み)、なにかしら対策をしておくのがいいでしょう。

まず第一に考えられるのが、アラートを設定することです。これは、料金の見積もりが設定した閾値の50%、90%、100%を超えた時点で管理者にメール通知を送ります。

しかし、この料金は厳密ではなく推定に基づいているため、いまいち信憑性がありません。また、アラートが飛んできたときにはもう手遅れという心配もあります。

なので、一定時間以上起動しているインスタンスを削除するスクリプトを定期的に実行しています。これなら、消し忘れで余計な費用がかかる問題を早い段階で防げます。

とはいえ、金額の上限設定は安全のためにほしいので、もしGCE関係者がこの記事を見ることがあったら、なにとぞご検討をお願いします。

その他の知見

なぜAWSを使わないのか

EC2も検討しましたが、今回の用途だとEC2インスタンスの最小課金時間が1時間なのがネックになります。自分たちの場合、常にインスタンスが起動しているわけではなく、1回ごとのインスタンスの起動時間は20分くらいなので、かなりの時間が無駄になります。

一方で、GCEの最小課金時間は10分で、それ以降は1分単位の課金になります。これなら無駄になることはほぼありません。

さらに、最近GCEのマシンタイプにカスタムタイプが追加されました。まだベータの機能ですが、インスタンスのCPUのコア数やメモリの量をある程度自由に設定できます。

これまでの定型のマシンタイプだとリソースを余らせ気味でしたが、カスタムタイプだと必要なリソースのみ割り当てることによってコストを下げることができます。長時間使用による割引も細かく計算しているようで、定型のマシンタイプと完全に同じ構成とかにしない限りはお得になります。

このように、現状ではコスト面のメリットが大きいので、GCEを選択しています。

ネットワーク

当たり前ですが、テスト対象のサーバーが社外からアクセスできないネットワークに存在する場合、Google Compute Engine上のインスタンスからはそのままではアクセスできません。インスタンスからアクセスできるようにサーバーのIP制限を変更する、VPNを構築するといった対策が必要になります。

他にも、Seleniumテスト内でブラウザから社内ネットワークなどにアクセスするところがあると同じ問題が発生します。自分たちの場合、社内のVCS上のファイルのURLを参照していたので、修正が必要でした。

権限管理

Google Cloud Platformの残念な点として、権限管理の設定が非常に大雑把です。

まず、アカウントの権限が「オーナー」「編集可能」「閲覧可能」の3つしか選べません。実際に運用してみると、もっと細かい粒度での権限設定がほしくなります。

リスクを減らすためには、「オーナー」「編集可能」権限を持ったアカウントの数を極力少なくするしかないでしょう。

今回のようにCIなどで活用するためには、前述したようにサービスアカウントなどを活用するのがいいでしょう。このとき、API Managerで可能な限りAPIを無効にしておいたほうが無難です。

Google Cloud Platformの変化が激しい

ここ最近、Google Cloud Platformは大きく変化しています。今回この記事を書くときも、UIやリソース割り当てなど大きく変わっていて驚きました。機能が増えていくのは素晴らしいことなのですが、少し前に書かれたベストプラクティスでも現在では最適でない可能性があるということは意識しておく必要があります。

なので、可能な限り、公式のドキュメントを読んで情報を仕入れてから作業しましょう。たまにドキュメントが古くなったままのこともありますが。。

IE

現状、IEではSeleniumテストを実行していません。Windowsはコンテナが利用できないので環境のメンテが大変ですし、Seleniumの挙動も微妙に異なっていてテストコードの修正コストもかかるという問題もあります。IEのみの大きな不具合は発生しているのか、手動テストではカバーしきれないのかなど、チームにおけるIEの自動テストの重要度と照らし合わせて運用するかどうかを決めましょう。どうしても必要となった場合でも、テストケースの数をできる限り絞ることを検討しましょう。

最新のWindows ServerはDocker対応しているという話ですし、Microsoft Edgeもいろいろモダン化されているので、将来的にはIEも手軽に自動テストできる未来が待っているのではないかと思います。Microsoft様どうかよろしくお願いします。

おわりに

Selenium記事でありながら、実体はほとんどGoogle Cloud Platformのノウハウになってしまいました。しかしながら、Seleniumテストを運用するためにはブラウザ環境のインフラ構築が重要なので、IaaS関連の情報を把握しておくことも役に立つことがあると思います。

実際、これまではブラウザ環境のメンテナンスにかなりの労力を費やしていたのですが、GCE移行後はほとんど工数をかけた記憶がありません。Selenium Gridに限らず、社外に出せるものはどんどんクラウドに持っていきたいと考えています。

クラウドをうまく利用して、皆さまもぜひ幸せなSeleniumライフを送りましょう!

kintone開発者のこだわりとは? kintone devCamp 2015でkintone開発の裏側を紹介しました

$
0
0

こんにちは。kintone開発チームの天野(@ama_ch)です。

12月4日(金) サイボウズ東京オフィスで開催されたイベントkintone devCamp 2015で、「こだわりのkintone -kintone開発の裏側-」というタイトルでkintone開発について発表させていただきました。

資料を公開しましたので、内容を補足しながらご紹介します。

kintone devCampとは

kintone devCampは、サイボウズが主催するdeveloper向けの勉強会です。kintoneカスタマイズ初心者~上級者の方を対象に、ハンズオン形式でkintoneのカスタマイズ方法を学びます。

今年から開始した勉強会ですが、すでに7回開催されており、年末スペシャル版としてコンテンツを拡大したのがkintone devCamp 2015です。

こだわりのkintone -kintone開発の裏側-

kintone devCamp 2015では開発チームで1セッションを担当して、kintone開発の裏側について紹介させていただきました。

90分のセッションでプログラマ、品質保証エンジニア、プロダクトマネージャーが計6人登壇し、それぞれの立場からkintoneのこだわりを語りました。

以下に埋め込まれた資料はすべて同じものですが、発表者ごとにページ番号を変えて表示しています。

1. kintone開発チームのkintoneの使い方 (天野 祐介)

最初は私からkintone開発チームと、kintone開発チームでのkintoneの使い方について紹介しました。

スライドに出てくるタスク管理アプリは社内でも最古のkintoneアプリで、これまでに約2200回設定が変更されています。業務の変化にあわせて柔軟に設定を変更できるkintoneの特徴を最も表しているアプリと言えるのではないかと思います。

2. kintone配属200日記 (前田 浩邦)

次に開発チームプログラマ配属2年目の前田が、配属からこれまでやってきた仕事を紹介しました。

日々タスクをこなしながら成長していく過程がまとまっていて、開発チームの業務についてイメージしていただけるのではないかと思います。

ちなみに彼は普段はたいていクレジットカードの話をしていて、最近は家計簿をつけることにハマっているようです。

3. わたしたちが正しいkintoneクエリにたどり着くまで (小林 大輔)

同じく開発チームプログラマの小林から、kintone REST APIの実装中に遭遇した様々な問題にどう取り組んだか紹介しました。

数百ケースのテストを書いて動作を確認し、他部署のメンバーとも連携してお客様への影響を最小限に抑える取り組みは、まさにkintone開発者のこだわりですね。

4. kintoneテストのウラ側 (渡邉 豊幹)

後半は品質保証(QA)エンジニアのパートになり、渡邉からkintoneのリリースサイクルにQAメンバーがどのように関わっているかを紹介しました。

kintone開発チームでは、仕様検討段階からQAメンバーも参加して試験設計をしたり、QAメンバーが設計した試験をプログラマが自動化したりと、品質向上を目指して各チームで協力して取り組んでいます。

5. セキュリティ標準から見る kintone のセキュリティ機能 (伊藤 彰嗣)

セキュリティチームの伊藤からは、kintoneのセキュリティに対する取り組みを紹介しました。

サイボウズではこれまでも脆弱性報奨金制度といった脆弱性を早期発見・改修する取り組みをしてきましたが、お客様により安心して利用していただけるように現在kintoneでは OWASP ASVS v3 というセキュリティ標準に準拠することを目指しています。

6. kintoneのバージョンアップを振り返る (齋藤 晃一)

最後に、kintoneプロダクトマネージャーの齋藤からこれまでのkintoneの変遷を紹介しました。

これまでの膨大なバージョンアップを振り返った後、「今後も技術者のみなさまと連携して一緒に新しいシステム開発を生み出していきましょう」と締め括られました。

昔のkintoneの画像が懐かしすぎて、これだけでオジサン達は数時間くらい語れそうな勢いでした。

まとめ

開発関係者がこのような形で発表することは今までなかったので、面白い取り組みだったと思います。今回のような機会を通じて、少しでも開発チームを身近に感じていただけると嬉しいです。

kintoneはこれからもユーザー・開発者のみなさまに役立つ機能を提供していきたいと思います。今後ともよろしくお願いします!

たのしい社内ハッカソン

$
0
0

f:id:cybozuinsideout:20151209153946p:plainこんにちは。開発部の刈川です。12月に入ってからどんどん寒くなってきましたね。こんな寒い日は一日中ハッカソンしていたいものです(?)。 というわけで今回は先日サイボウズ内で3日間にわたって開催された社内ハッカソンについてご紹介したいと思います。 ハッカソンの定義についてはこちら

どんな感じでやったの?

以下の様な概要で開催しました。

  • 開催期間は3日間
  • 期間中はハッカソンに集中(緊急の業務があればそちらを優先)
  • 個人もしくは3人チームを組んで参加
  • 東京、大阪、松山の各拠点で開催
  • 大賞には最大50万円分の自己投資の権利※を贈呈

※海外カンファレンスへの参加費など個人の成長に繋がるもの

準備

  1. アイデア箱にやりたい/やってほしいネタを投稿する。
  2. アイデア箱に登録されたネタをみんなで眺める。
  3. やりたいネタがあったら申告。なるべく他の参加者と被らないようにする。

アイデア箱に登録するフェーズは「アイデアソン」とも呼ばれています。 非参加者でもネタの投稿が可能なので、ビジネスサイドからの要望など開発サイドにはない視点で新しいアイデアが生まれやすいのが特徴です。 f:id:cybozuinsideout:20151209161647p:plainアイデア箱の様子。

チーム編成

チームのメンバーは完全にランダムで決まります。ほとんど業務上で関わりが無いようなメンバー同士が繋がったり、得意な分野が違うメンバー同士が繋がったりするので何が生まれるのか分からないワクワク感が生まれます。

開催当日

  1. おやつを用意する。
  2. ひたすら実装
  3. 2日目に中間発表を設ける
  4. 3日目に5分/チームの成果発表

各自の作業場所はラウンジ、自席など様々でした。

f:id:cybozuinsideout:20151209163047j:plain:w240f:id:cybozuinsideout:20151209163056j:plain:w240f:id:cybozuinsideout:20151209163107j:plain:w240f:id:cybozuinsideout:20151209163621j:plain:w240

どんなのを作ったの?

社内で開かれるハッカソンということもあり、ネタは自社製品のカスタマイズや社内で起きている不便なことを解決するためのサービスなどが多めでした。 一部をご紹介します。

社内ATNDサービス

f:id:cybozuinsideout:20151211113248p:plain社内イベント開催支援ツール。おおよそはATNDconnpass等のサービスと同じですが、それをさらに社内用に特化したサービス。 サイボウズではよく社内勉強会が開かれるのですが、いつどこで開催されているのか分からない、気づけないと言った問題があったため、自社グループウェアと連携して社内で開かれている勉強会情報をまとめて一覧できるように。

発表中のコメントをニコニコ動画風に表示するツール

f:id:cybozuinsideout:20151211160136p:plain

プレゼン中に聴衆がスクリーン上にコメントを流せるようにするデスクトップアプリ。コメントはkintone上に作られた実況スレッドの書き込みを拾って表示しています。Windows版、そしてElectron版も用意するというハイブリッドっぷりです。

Pepper君にいろいろ喋らせるやつ(その他IoT)

f:id:cybozuinsideout:20151211135137j:plain

スレッドに書き込まれた内容をPepper君が喋ってくれます。これとは別の参加者はウォーターサーバの使用状況をセンサーで検知して喋らせる「しゃべるウォーターサーバ」などを作っていました。

社内Wiki + 編集ツールの開発

f:id:cybozuinsideout:20151211141419p:plainkintoneをバックエンドにwikiのシステムを構築し、WYSIWYGな編集アプリをセットで開発。Markdownも使えて嬉しい。

通知撃破型シューティングゲーム

f:id:cybozuinsideout:20151211140854p:plainグループウェア上の通知を敵と見なし、シューティングゲームで撃破することで既読にしてしまおうという無慈悲なゲーム。ちなみに被弾すると未読に戻ります。

発表の様子

発表は東京、松山、大阪の同時開催。

f:id:cybozuinsideout:20151211142130j:plain:w240f:id:cybozuinsideout:20151211142151j:plain:w240

各拠点はテレビ電話会議システムで常時接続しています。 f:id:cybozuinsideout:20151211142347j:plain

その他、細かい運営の話とか

今回からハッカソン運営委員を設けて準備を進めました。運営委員は各拠点から1名、委員長で1名の計4名です。やったことは以下です。

  • 予算の確保
  • 参加エントリーフォームの作成
  • アイデア箱の作成
  • チーム編成(ランダム)
  • 審査委員の選定
  • おやつの準備
  • 各種告知
  • 発表会準備
  • 写真撮影
  • ふりかえりの実施
  • 開催報告(このブログです)

来年も開催できるように参加者からのフィードバックを集め、ふりかえりを行うことが重要です。

まとめ

f:id:cybozuinsideout:20151211145010j:plain:w240f:id:cybozuinsideout:20151211145514j:plain:w240

毎年の恒例行事になりつつある社内ハッカソンですが、出てくるネタも年を追うごとにバラエティに富んだ個性的なものになっています。 初回は十数名しか参加がなかったのですが、今年は35人を超える方に参加してもらいました。 普段の業務からいったん離れて自分の作りたいもの、やりたいことに取り組めるのでリフレッシュにもなります。 そして、ハッカソンで産まれたものがそのまま業務で役立つようになるというケースも増えてきており、開発方面以外にもバリューを届けられるようになってきました。 ゆくゆくは社内外を巻き込んで楽しくHackできるようなイベントに成長していけたら良いなと思っています。

初めは小規模でも良いので皆さんも社内ハッカソン、やってみてはどうでしょうか。楽しいですよ!

インフラ系インターンをやってみました

$
0
0

皆さんこんにちは。Hazama チームの内田(@uchan_nos)です。

かなり前の話になってしまいますが、サイボウズでは 8/17 から 1 週間、開発系のインターンシッププログラムを実施しました。 アプリ開発や品質保証のコースに加えて インフラコースがあったのが、サイボウズのインターンとして初の試みでした。

f:id:cybozuinsideout:20151104124309j:plain

私が所属する Hazama チームは、サーバの運用管理用のツール(デプロイやモニタリングを行うツール)を作っています。 今年の初めにはデータセンターのフロントエンドを Apache から nginx に切り替えたりもしました(cybozu.com のリバースプロキシを nginx にリプレイス - Cybozu Inside Out | サイボウズエンジニアのブログ)。

インフラコースは Hazama が担当するということで、私が 3 人の学生さんたちのメンターとしてインフラに関する講義をしたり、課題達成に向けてサポートを行うことになりました。 上手くいった点や次回に改善したい点などをご紹介します。

目次

インフラコースでやったこと

インフラコースといっても、ネットワークを配線したり、サーバにログインしてバックアップなどのオペレーションをしてもらうの ではなく、 クラウド環境のソフトウェアを整備するような課題を設定することにしました。 何かと地味な印象のあるインフラですから、その中でも学生さんの興味を引けて、かつ実用になるような ワクワクする課題を探しました。

最終的に用意した課題は「LXC コンテナを使ってローカル PC 上にミニクラウド環境を作ろう」です。

f:id:cybozuinsideout:20151029155601p:plain:w300

目指すのは nginx をフロントエンドにして 2 台の AP サーバがロードバランスされるシステムです。 nginx と AP サーバ用のホストマシンはそれぞれ LXC コンテナとして構築し、3 台のコンテナを Linux Bridge で繋ぎます。 インフラコースといってもサイボウズは Web 企業ですし、 AP サーバは学生自身で簡易な Web アプリを作ってもらうことにしました。

そして、このシステムを手動で構築して終わりではなく、 0 から自動的に構築するデプロイスクリプトの開発を最終成果物としました。 インフラプログラマは、こういった自動構築の仕組みを作るのが大きな役目です。

まずはインフラコースで起こった面白いエピソードを幾つかご紹介します。

LXC の IP アドレスをどうやって取るか

LXC コンテナの IP アドレスを取得する方法、みなさんご存知でしょうか。

LXC コンテナは初期設定でコンテナを作ると DHCP で動的に IP を取得する構成になります。 nginx からロードバランスするには AP サーバのホストの IP アドレスを nginx が知っている必要がありますから、 DHCP を有効にしたまま何とかするには DNS を使ったり、 あるいは nginx の設定ファイルを AP サーバのコンテナを再起動するたびに書き換える必要があります。 もちろん、各ホストに静的アドレスを割り振るのもアリです(そして、それが一番楽です)。

$ sudo lxc-ls --fancy
NAME          STATE    IPV4        IPV6  AUTOSTART
--------------------------------------------------
ubuntu-ap1    RUNNING  10.0.3.128  -     NO
ubuntu-ap2    RUNNING  10.0.3.106  -     NO
ubuntu-nginx  RUNNING  10.0.3.252  -     NO

学生さんたちがどんな方法を使うかなーと観察していたところ、3 人とも AP サーバの IP アドレスを動的に取得する方法を選択したようです。 文字列の split 関数などを使って lxc-ls --fancyの出力を 頑張ってパースするコードを書いていました。

パースしなくても lxc-info -iHn $NAMEで IP アドレスだけを取得できるんですけどね。(私もそのときは知りませんでしたが!)

なぜかコンテナ内で apt-get が失敗する

ある学生さんは、まず LXC コンテナを生成し、その後 lxc-attachを経由して apt-get installを実行するデプロイスクリプトを書きました。 すると何故だか apt-get が失敗してしまいました。

彼はその原因がネットワークの不安定さにあると突き止め、APT リポジトリに定期的に ping し、ネットワークの立ち上がりを認識するという修正を加えました。 その修正にはタイムアウト処理も含まれるという完璧さで、この問題は無事回避されたのでした。

インフラのソースコードには

# 削除すると動かない
sleep(3)

みたいなコードが 必ずあるもので、インフラのチームに配属された新人はなぜ 3秒待てばよいのかまったく理解できないものです。 彼は学生にして、その深淵な世界を知ってしまいました。 この所為でインフラエンジニアになる夢を捨ててしまわないように祈るばかりです。 (そんなコードを書いたことがないなら、それは下層システムを作っている人が頑張っている証拠ですから労ってあげましょう!)

インターンをやってみて、上手くいったところ

今回のインターンでは、サイボウズは 技術力が高くて、エンジニアが働きやすい会社だと分かってもらうことを目標にしました。 その会社で働くということの具体的なイメージを持ってもらい、入社したいと思ってもらえたら成功です。

そのために、まずは学生さんにインターンを 楽しんでもらうことが大前提と考え、そのために課題の準備から当日の運営までいくつかの工夫をしました。 楽しめないインターンではマイナスイメージを与えてしまうでしょう。本当は技術力が高くて働きやすい会社であっても。

その上で、サイボウズの技術力をアピールするための工夫を入れることにしました。 インターンは楽しいし会社もすごいとなれば、入社したいと思ってもらいやすくなるはずです。

その甲斐あってか、インフラとして初めてのインターンにもかかわらず、参加された学生さんから高い評価をいただきました。 功を奏したと思うポイントを挙げていきます。

面白い課題を見つける

面白さにはいくつかの観点があります。

まず、勉強のための課題ではなく、その会社の役に立つ課題であるかどうか。 会社に役立ちそうだと感じれば、課題をこなすモチベーションが高まるでしょう。 さらに、それが 本当に役立つものなら、彼らの成果を実際の業務に組み入れることができます。

言語の入門書を与えてお勉強してもらうのを課題にするのではなく、業務に関連したコンテンツを盛り込む。 そういう考えのもと、今回のインターンではサイボウズのインフラで 近い将来使うであろうコンテナを用いたロードバランシング機構を作る課題を設定しました。

(その課題達成のために Python を勉強する必要があるなら、その時は堂々と「空飛ぶ Python」を読んでもらうことにします。)

次に、技術的にも面白い課題かどうかという観点があります。 その時々のトレンドになっている技術は、世間に知られ始めたばかりで挑戦したくなる技術が多いです。 今回のインターンを開催したときは、コンテナ技術が世間に浸透し始めていました。

最後に、ちょうど達成できるくらいのレベルになるように設計をしました。 課題が難しすぎても簡単すぎてもつまらなくなってしまいます。 挑戦しがいがありつつ、まったく歯が立たないことがないようにするため、チームメンバーと議論して課題を詰めました。

課題の難易度を調整する

といっても、課題を簡単にするのが難しい場合もあります。 そういうときは正解のひな形を用意しておくのが一つの解決策です。 今回は、LXC コンテナの細かい動作原理は省き、コンテナを作成するコマンド列を準備しておきました。 また、Web アプリの作成経験がない場合に備え、ロードバランス対象の Web アプリのひな形を配ったりしました。

課題のレベルを適切にするためには 学生さんの技術的な経験について事前に把握しておく必要があります。 面談で技術的な質問をするとか、簡単な課題を解いてもらうなどの方法があるでしょう。 今回私は、学生さんたちが現在取り組んでいる研究に関することや Linux に関することを質問しました。 Python によるプログラミング経験、Linux コマンドラインの経験の有無などは、課題のレベル調節に欠かせない情報です。

講義の中でも手を動かす

インフラ系の知識は学生時代に知ることがあまりないため、今回のインターンでは講義の時間を多く取りました。

講義といっても 録音を聞いた教授本人さえ寝てしまうような講義ではなく、 積極的に学生さんたちにクイズを出し、ときどきコマンドを打ってもらいつつ進めました。 クイズを解くというのは 能動的に答えを探すということですから、脳が活性化して知識をよりよく身に付けられます。

このようなインタラクティブな講義は、事前に用意した資料をただ読んでもらうのでは実現できず、メンターの頑張りと、インターンに割ける時間が多いことが求められます。 学生さんたちに喜んでもらえるインターンとするために、私はその週、その他の業務を できるかぎり排除して、講義に時間を使いました。 メンターが良い講義をすることで、会社の技術力のアピールにもなります。

煮詰まり状態を解消する

  • RuntimeError が 38 行目で発生したことは分かったが、果たして本当の原因は何なのか。コードを眺めてもさっぱりだ。
  • コンテナの IP アドレスを得る方法がそもそも分からないぞ…

というとき、人は煮詰まります。煮詰まったまま PC の前で数時間が経過する、なんてこともあるでしょう。 インフラコースの学生さんたちも、そういう経験は何度もあるとのこと。 私の経験的に、一度煮詰まると、知ってる人から見ると筋が悪い試行錯誤を繰り返して時間だけが過ぎてしまうのが常です。

ということで、私はメンターとして 30分ごとに休憩を促しに行くことにしました。 30分というのは、時間管理テクニックで知られる「ポモドーロ法」で良く使う単位です。 私が自席で30分タイマーをかけ、それが鳴ると学生さんの席まで歩いていって、席を立って近くを散歩してくるように伝えます。 この人間タイマー法はとても好評でしたし、何しろ私自身が、彼らが何かに煮詰まっているかどうかを観察できる、優れた手法でした。

メンターから話しかける

学生さんたちは企業のオフィスに来るというだけで緊張しているはずですし、なかなか聞きたいことを質問できない雰囲気になっているかもしれません。 ことあるごとに「調子どう」と なるべく軽い雰囲気で声をかけ、問題が小さいうちに解決できるようにしました。

「煮詰まり状態を解消する」は自分でタイマーを仕掛けてもできますが、 緊張をほぐしたり質問しやすい環境を作るには、やはりメンターから話しかけることが必要です。 スムーズに開発できる環境を用意することで、学生さんたちにとっても私にとっても、 インターン期間を充実した楽しい時間にすることができたのではないかと思います。

同僚の協力をあおぐ

学生さんたちはメンターである私 以外の社員についても知りたいと思っているはずです。 近い将来、一緒に働く仲間になる可能性がありますから! そこで、私のチームの他のメンバーに協力を頼み、インターンに関わってもらうことにしました。

ただ、「関わる」と言ってもおしゃべりをするだけでなく、 コードレビューをしてもらうことにしました。 チームメンバーにレビューしてもらうことで、サイボウズという会社に、私以外にも技術力がある社員がいることを宣伝できます。 また、学生さんたちとのやりとりで忙しい私に代わって、じっくりレビューをしてもらえるという良い副作用もありました。

開発メンバーの一員として受け入れる

学生さんたちはその企業がどんな雰囲気なのかを知るためにインターンに来ています。 それなのに、周りの社員と異なる環境でインターンの期間を過ごすのでは、雰囲気を良く知ることはできません。

サイボウズはエンジニアに 素晴らしい椅子と机を支給している会社ですから、学生さんたちにも同等のものを使ってもらいました。 また、サイボウズの社員はお互いに仲が良いですから、その様子が見えるように壁で仕切られていない場所に席を確保しました。 さらに、昼食を食べながら自主的に勉強会をする文化がありますので、「Rと統計の勉強会」に学生さんたちを誘いました。

インターンの途中の日には、気軽に質問し会社のことを知ってもらうためにインフラチームの社員も誘って外食に行きました。 社員同士で外食に行くのは、もちろん普段から良くあることです。

改善点

上手くいったことだけではなく、改善すべき点もいくつかありました。 とくに、事前準備を入念に行うことが大切だと感じました。

課題の選定、資料の準備などはパッと思いつくところで、今回のインターンでも忘れずに行えました。 反省は、それ以外の 準備を見逃しやすい事柄をまんまと見逃してしまったことです。

開発マシンの手配と設定

開発マシンとモニタが学生の人数分だけ確保できているかを、追加の発注が間に合うくらい余裕をもって確認すべきでした。 今回は、モニタの手配が直前になってしまって、社内からかき集めてきて間に合わせましたが、 当日になってもモニタが足りなかった可能性がありました。

インフラコースでは、普段私たちがやっているように開発マシンの Windows 上で Hyper-V を設定し、Linux を入れて開発に使ってもらうことにしていました。 本当は、事前に設定を済ませておくつもりでしたが、開発マシンの座席への設置が直前になってしまったことから、 仕方なく学生さんたちに設定作業をお願いすることにしました。

しかし、手配したマシンの BIOS で仮想化技術が無効になっており、Hyper-V で仮想マシンを作ることができず、苦労しました。 まず、BIOS に問題があることを突き止めるところから始まり、貴重なインターン期間を余計な作業に費やすことになってしまいました。 (なんと BIOS の「セキュリティ」という項目の中に仮想化技術の設定があり、探し出すのにとても時間がかかりました。)

アカウントの手配

サイボウズの開発で使っている GitHub Enterprise を学生さんたちにも使ってもらおうと思っていました。 ただ、それは 開発側が思っていただけで、インターンを統括する人事部はそんなことは感知していませんでした(いやまあ、当たり前なのですが)。 かなり直前になってアカウントの追加申請をしていないことが発覚し、情報システム部に急いで頼みましたが、 たまたま GitHub Enterprise の 人数制限に達しておりアカウントを追加できませんでした。

今回は社外秘なコードを書くわけでもありませんでしたので、github.com 上で開発を行うことにしました。 インターン終了後にも学生さんが自分のコードを見られるようになり、逆に良かったかもしれません。 ただ、もし社外秘なコードを書くことになっていたら、課題設定を見直す必要があったでしょう。

Office 365 などを社内で使っており、それらをインターンの発表資料作成などで使ってもらおうと考えているのなら、そのアカウントも要チェックです。

おわりに

サーバ仮想化って何のためにやってるか、という質問を最初にしたとき、学生さんたちは「リソースの有効活用のため!」と答えてくれました。 そういう目的もなくはないのですが、サイボウズで最も大事なのは 物理機材が壊れたときのための仮想化だということです。 サービスを仮想マシンやコンテナの上で動かしておけば、故障時に素早く他の機材上で起動しなおすことができます。 その視点はかなり新鮮なものだったようです。

インターンを通して、クラウドインフラが動く仕組みやインフラ向けプログラムを作る際の気構え、 ネットワークのデバッグ技術、コンテナ技術など、多くの学びがあったのではないかと思います。 個人的にも、初めてインターンシッププログラムで学生さんをサポートする立場で、 一緒に技術のお勉強をしたり実りの多い 5 日間でした。

インフラコースではありませんが、kintone 開発チームでは通年でインターンシップを募集しています。 サイボウズのインターンシップ全般を詳しく知りたい方は インターンシップ | サイボウズ 採用情報(新卒・キャリア)をご覧ください。情報は随時更新されます。

サイボウズLiveの新アプリのAndroid版が公開になりました

$
0
0

こんにちは。サイボウズLiveチームの山川です。

12月14日に公開となったサイボウズLiveの新アプリ「サイボウズLive TIMELINE」のAndroid版の設計および実装を担当しました。

サイボウズLive TIMELINE - Google Play の Android アプリ

f:id:cybozuinsideout:20151217172419p:plain

Androidユーザーの方は是非ダウンロードをよろしくお願いします。このエントリでは、Androidの新アプリについて実装面でのお話をしようと思います。

はじめに

今回の新アプリはサイボウズLiveチームにおいて、Androidアプリの初のネイティブアプリ開発(既存のアプリは Titanium )でした。そのため、開発期間の最初の1ヶ月間ほどは調査期間として、必要になるであろう機能を簡易実装したサンプルアプリを作りました。サンプルアプリといえど1ヶ月の期間があると、それなりのボリュームの物が出来上がります。そして、気づいたころには Activityが肥大化していました。メソッドの切り出し等のリファクタリングをするにしても、あまりスマートになる気がせず・・・。

これは本番実装ではもう少し賢く書かないと後々大変だと思い、もろもろ調べたり人に聞いたりして、最終的に「 Model view presenter(MVP) + Domain-driven design(DDD)」のアーキテクチャを意識した構成となりました。 こちらの記事などはとても参考になりました。

MVPとDDDに関しての詳細説明は割愛します。ここではMVP+DDDをAndroidで実装した話をまとめます。

パッケージ構成と全体処理の流れ

本アプリのjavaパッケージ構成は以下のようになっています。

├data
│├dxo (XML→entity)
│├repository (domain/repositoryの実装)
│└util (repositoryの実装で使ういろいろ)
│
├domain
│├entity (サイボウズLive固有のエンティティ)
│├repotisory (entityのCRUDインターフェイス)
│└usage (repositoryの利用の切り出し)
│  
└ui
  ├dxo (entity→vmo)
  ├presenter (presenterインターフェイス)
  │└impl (presenterの実装)
  ├view (viewインターフェイス)
  │└activity (viewの実装)
  └vmo (表示用エンティティ要約オブジェクト)
    

各パッケージの詳細は追って説明します。全体的な処理の流れを以下の図にまとめます。模式図ですが、処理の流れと各層の処理内容がお分かりいただけると思います。

f:id:cybozuinsideout:20151217110355p:plain

各パッケージの役割

domain アプリケーションの中心

アプリケーションの中心となる domain 層は entity と repository が主な要素です。

entity はサイボウズLive固有の entity です。Androidとは無関係なメンバー変数に持つことになります。 ただし画像情報などを担う entity は画像の保存先のファイルパスなどを保持することはあります。

repository は基本的には entity のCRUDを提供するインターフェイスとなります。以下に「Group」の entity をリストで取得するインターフェイスの例を示します。

publicinterface GroupRepository {

    void getGroupList(OnGetGroupListFinishListener listener);

    interface OnGetGroupListFinishListener {
        void onGetGroupListFinished(List<Group> groupList);
        void onError(ErrorType errorType);
    }
}

Androidの制約でメインスレッド上でのネットワーク接続は禁止されています。したがって通信が発生する処理は、非同期処理となります。本アプリでも多くはAPI経由でデータを取ってくるものなので、それを考慮したうえで、コールバックインターフェイスにしておきます。

usage パッケージは、repositoryの利用でコールバックを受け取り、そのコールバックで再び repository を利用するような、多段のコールバックとなってしまうような処理をまとめておくものです。これは、単純にコールバックの連続はコードとして読みにくいということと、複数個所から全く同じ repository の連続利用がある場合に一連の処理を切り出して共通化したい、という理由からです。DDDの考え方だと、UseCase を設けてそこのクラス経由で repository 利用するのが一般的かつ、重要なのですが、本アプリでは実は UseCase は存在しません。その理由は後述しますが、UseCase がビジネスロジックの分離を目的としているのに対して、usage は単純な利用の切り出しといった位置にあるため、(将来、コードを見る人が勘違いしないように)異なる名前にしておきました。

data domianの実装

domain の repository インターフェイスを実装します。その多くはAPI経由でデータを取得して entity を作成することです。API通信以外にも、ローカルデータの場合は SharedPreference、アイコンや画像を扱う場合はファイルへの書き込みと読み込みを行う場合もあります。SharedPreferenceのキー名の管理や、ファイルパスの管理は repository インターフェイスを介すことで、data 層内に集約することができます。非同期処理、通信、XMLのパースや組み立て、OAuth用の汎用処理は data 層の util 内に配置します。 dxo はAPIの結果のXMLを entity に変換するクラスを配置するパッケージです。

ui view の実装をするActivityとpresenter

ui パッケージにはまず、view インターフェイスと presenter インターフェイスという2大要素があります。view インターフェイスの実装はActivityやFragmentが担います。view の構築およびイベントリスナーなどを配備するのが仕事です。イベントは presenter に伝えます。 presenter は view の参照を持ち、Activityからのイベント呼び出しを待ちます。イベントが呼び出されてたら、必要な処理を実行し、その結果をviewインターフェイスを介して反映させる、というのが presenter の役割です。当然処理内容に repository の利用がある場合は、コールバックを受け取るのは presenter の実装クラスとなります。

repositry から entity や entity のリストを取得してそれらをviewに渡す際に登場するのが vmo (View Model Objectの略、筆者が命名)です。これは「 entity を表示用に要約する」という位置づけのものです。例えば、グループのメンバー名簿を表示することを考えます。表示の仕様は

  • メンバーの姓名を1行目に表示

  • 2行目には最終ログイン日時を最終ログイン:○月○日」と表示、年が操作時と異なる場合は年も表示

  • グループ管理者の場合は姓名を太字にする

とします。グループメンバーの entity が姓、名、最終ログイン日時、管理者フラグを持っていれば、これを仕様通り表示させるのは造作もないことです。しかしこれを view の実装でだらだら書くと、 レイアウトwidgetの作成と混在することになります。そこで view に渡す前に、vmo で要約してあげることで「表示したいもの」の作成と「表示すること」を分離します。以下にこの例における entity と vmo の模式図を示します。

f:id:cybozuinsideout:20151217110349p:plain

vmo はとにかく「何をどう表示するか」を扱いやすくなっていればいいので、変数名などは必ずしも中身と相関しなくてもいいと思います。この辺りは分離しなくても問題ない場合も多いですが、多言語対応など将来的に表示に関わる改善を見越したときのことを考えると、やっておいて損はないと思います。

UseCaseがない理由

本アプリではDDDを意識したと言いつつ、DDDで重要とされる UseCase がありません。その理由として、すでにビジネスロジックを解決済みのAPIを利用しているから、ということなります。つまり、APIがすでに UseCase の外側に位置するので、Androidアプリ内ではAPIの結果を entity に変換した時点でビジネスロジックの分離が完了しているのです。ただこれは、APIの提供側とAPIの利用側が同じ目的を達成したい場合に限ります。利用するAPIがアプリの達成したいロジックを提供していない場合、当然ながら UseCase を設けることは大変有効だと思います。

MVPパターンにするメリット

格段にコードが読みやすくなります、と言うと主観的な感想になってしまいますが、view インターフェイスや presenter インターフェイスを設けることで、データの取得( repository の利用)と view の構築が自然と分離されるのは大きいです。またAndroid依存の実装箇所も自然と分離されるので、保守性の観点からもうれしいです。

MVPパターンにするデメリット

まさにデメリットというものはあまりないですが、そもそもActivityクラスというものが view の実装専用のクラスではないという点は懸念材料です。今後Androidプラットフォームがどのように進化していくかはわかりません。Activityが view の実装という割り切りを崩す必要があるような変更が将来あるかもしれません。

おわりに

今回はAndroidアプリの実装についてお話させていただきました。とりわけアーキテクチャというものは、抽象理論の時点だと「なるほどー」となりますが、実際うまい具合に実装できるかは、やってみないと分からないことが多々あります。さらにそれがAndroidという開発者を悩ませることの多いプラットフォーム上となると・・・。

そんなAndroidですが、世界シェアではiOSより全然大きかったりします(日本は違いますが笑)。 苦労する分、工夫のしがいはあるよ!という前向きな言葉を結びといたします。

皆さまもぜひ幸せなAndroidライフを送りましょう。

もうサムネイルで泣かないための ImageMagick ノウハウ集

$
0
0

こんにちわ、アプリケーション基盤チームの青木(@a_o_k_i_n_g)です。好きなみかんは紅マドンナです。

今回は、サイボウズのサムネイル事情について記事を書きたいと思います。サイボウズに限らず通常の Web アプリケーションでもサムネイル作成はよくあると思いますが、ハマりどころが多く涙しているサムネイリストも多いかと思います。これからの時代を生きるサムネイリストが快適なサムネイルライフを送れるよう、知見を共有したいと思います。

弊社では画像変換ツールに ImageMagick を用いており、従って本知見は ImageMagick 固有のものがほとんどです。

画像比較は人間の眼で行うべし

サムネイル周りに何か修正を入れたら修正前後の画像を比較しましょう。機械によるバイト列の比較では画像の良し悪しがわかりません。頼れるのは人間の眼だけです。肉眼で確認しましょう。

比較できるツールを作ると良いです。たとえばサイボウズでは下記のように、ブラウザで修正前後の画像一覧を見れるようにしています。二枚一組で、三列表示にしてます。 f:id:cybozuinsideout:20160104162401p:plain

透過画像がわかるよう、背景画像に何かしら色を入れたり、画像にボーダーを入れておくと確認しやすいです。

リソース大量消費に注意

高画素な画像や、画像サイズは小さくとも数千枚の画像を連結した GIF アニメの変換は大量のリソースを消費します。すると OutOfMemory で死んだり大量のディスク IO が走ったりします。これは言い換えると、サムネイル作成処理は DoS の穴になり得る、ということです。必ずリソース制限を行っておきましょう。

ImageMagick では -limitオプションでリソース制限ができます。-limitオプションは入力画像を指定するよりも前に指定しないと制限がうまく効かない場合があるので注意。ImageMagick 6.7.2 以前のバージョンは単位の MB を小文字で書いても大丈夫でしたが、6.7.3 以降は大文字でなければなりません。小文字で書くとミリバイト扱いされてしまいます。

convert -limit memory 256MB -limit disk 0 src.jpg dst.png

また、-limitオプションで画像変換 1 回あたりのリソース消費を抑えても、大量に同時実行されたら意味がありません。画像変換の並列度にも制限をいれましょう。

Orientation を考慮しよう

JPEG の場合は EXIF 情報に Orientation 情報が入っていることがあります。ImageMagick で変換する際は -auto-orientオプションをつけて向きを補正しましょう。Orientation 画像の確認は MS ペイントがオススメです。なぜなら、ペイントはシンプル故に Orientation を考慮した向きの補正を行わず、画像のバイト列の向きそのままの表示がされるからです。

EXIF に Orientation の項目が存在しながらも、値が入っていない場合があります。Orientation 値を抜き出して何らかの操作をする場合、null チェックが必要です。

Orientation 画像はこちらのサンプルが便利です。 http://www.galloway.me.uk/2012/01/uiimageorientation-exif-orientation-sample-images/

ただ、ImageMagick といえど Orientation 画像の変換でちょっと怪しい挙動があります。上記サンプル画像の right-mirrored.jpg を -auto-orientをつけて png に変換すると、offset 情報がおかしくなります。

$ convert right-mirrored.jpg -auto-orient out.png
$ identify out.png 
out.png PNG 480x640 640x480+160+42949671368-bit PseudoClass 256c 13.8KB 0.000u 0:00.000

このケースは -auto-orientをつけて一度 JPEG に変換し、改めて PNG に変換すると正しい情報の画像が得られます。

$ convert right-mirrored.jpg -auto-orient jpg:- | convert - out.png
$ identify out.png
out.png PNG 480x640 480x640+0+08-bit PseudoClass 256c 14.6KB 0.000u 0:00.000

透過画像を考慮しよう

PNG や GIF は透過色をサポートします。透過画像をそのまま JPEG に変換すると背景色が黒になるので、白にしたい場合は同じサイズにリサイズしましょう。

$ identify src.png
src.png PNG 800x600 800x600+0+08-bit DirectClass 218KB 0.000u 0:00.000
$ convert src.png -resize 800x600 -extent 800x600 dst.jpg

出力する形式も透過色をサポートしている場合は -background transparencyを付与するとトラブルが少ないです。

CMYK 画像を考慮しよう

画像データの色空間が CMYK の場合があります。CMYK 画像をリサイズしたりすると色味が変わることがあるので、-colorspace RGBをつけましょう。

また、CMYK 画像は Java での通常操作では取り扱いできないというのもハマりどころです。

通常はこのような操作で読み書きできます。

File file = new File("/path/to/cmyk.jpg");
BufferedImage image = ImageIO.read(file);
....

ですが CMYK の場合例外が出ます。

Exception in thread "main" javax.imageio.IIOException: Unsupported Image Type
    at com.sun.imageio.plugins.jpeg.JPEGImageReader.readInternal(JPEGImageReader.java:1063)
    at com.sun.imageio.plugins.jpeg.JPEGImageReader.read(JPEGImageReader.java:1034)
    at javax.imageio.ImageIO.read(ImageIO.java:1448)
    at javax.imageio.ImageIO.read(ImageIO.java:1308)
    at com.cybozu.common.Sample.main(Sample.java:15)

これを回避する方法もありますが、CMYK 専用のコードを書くよりは、ImageMagick に一任してしまったほうが効率的です。

グレイスケール画像を考慮しよう

白黒画像を PNG に変換すると、元画像より暗くなる場合があります。これは減色アルゴリズムによる挙動と思われます。JPEG はフルカラー画像を扱えますが、通常の PNG だと 256 色しか扱えないのです。

出力形式に PNG24 または PNG32 を明示的に指定して色空間を広げれば画像が暗くならずに済みます。ただし代償としてファイルサイズは大きくなります。

下記画像は、左から順に、元画像, $ convert gray.jpg dst.pngの結果, $ convert gray.jpg png32:dst.pngの結果です。ちなみにこの画像は弊社社員の近影です。

画像サイズの取得は ImageMagick で行おう

画像の変換は ImageMagick に任せるとして、画像サイズの取得はアプリケーションのコードで行いたくなるかもしれません。しかし前述したように Java の ImageIO は CMYK 画像の扱いに難があり、通常のコードではサイズの取得が行えません。また、同様に GIF についても JDK-7131823 : bug in GIFImageReaderのバグで読めないことがあるので、サイズの取得といえど信頼と実績のある ImageMagick に任せるべきです。

ただ、画像サイズを取得するコマンド、ImageMagick の identify も -verboseオプションをつけるとメモリ大量消費することがあるので注意です。これは色の統計情報を取得しているためでしょう。-verboseをつける場合は、identify コマンドにも -limitオプションが効くので指定しましょう。

ImageMagick のオプションの順序に注意

オプションは前から順に評価されていくので、順序によって結果が異なります。-resize-extent等の順序が異なると結果も異なるというのは直感に従いますが、たとえば背景色を指定するオプション -backgroundなんかも順序によってオプションが効いたり効かなかったりすることがあります。繰り返しになりますが、サムネイルは肉眼で確認しましょう。

ImageMagick の -define jpeg:size に注意

下記ブログで -define jpeg:sizeの有用性が示されています。 http://blog.mirakui.com/entry/20110123/1295795409

確かに条件がマッチすれば大変素晴らしいオプションです。ただ、変換後のサイズが大きい場合、たいていは元画像サイズの 1/3 以上程度ですが、これを超えると -define jpeg:sizeオプションを指定していない時以上にメモリを消費します。いつでもつければ良いというものではないので注意しましょう。弊社では、このオプションはサービスの安定運用のためには無用と判断し、現在このオプションは利用していません。

ImageMagick その他

ImageMagick のオプションは膨大です。そのせいもあって、ドキュメントが間違えていることがあります。そんな時はそっとページ下部の Contact US をクリックしてレポートしましょう。すぐ反映されます。でも返事は来ないです。

まとめ

ImageMagick はオプションが複雑でわかりにくい部分もありますが、それでも様々な画像を扱えるという信頼は揺るぎません。Java やその他言語の各種ライブラリでも画像データを扱うことはできると思いますが、様々な画像に対応することを考えるとやはり ImageMagick に一任するのが良いように思います。

それでは、よいサムネイルライフを!

半年かかったバグ調査の顛末は

$
0
0

サイボウズ・ラボの光成です。

今回は原因究明に半年以上かかったバグ調査の紹介をいたします。

弊社はクラウドサービスcybozu.comを提供しています。 クラウドサービスでは障害対策のためのデータバックアップやレプリケーションが必須です。 現在ラボの星野がメイン、私はサブとして弊社サービスでの利用を目指した次期バックアップシステムWalBGitHub)を開発しています。

WalBは、ファイルシステムとdiskの間に入ってIOを全て記録するブロックデバイスとIOのログを管理するツールからなるシステムです。 詳細はリンク先をごらんください。
walb-architecture

発端

去年はラボ内の開発環境でテストを進め、本社でテスト運用を開始するのが目標でした。 ところがラボでテストを開始して4カ月後の2015年4月、不正なlogpackが検出されました。 logpackとはWalBで用いられるデータフォーマットの一つです。 ログをみるとlogpackが壊れていたのでしばらくreadをリトライし、正常に読めたためシステムは継続していました。

このエラーは一度発生しただけで、かつ致命的なものではなかったのでしばらく様子を見ることにしました。 すると20日後ぐらいに再度エラーが報告されます。

これはきちんと原因追求しないといけないということで、WalB kernel driverのレビューをやり直したり、チェックコードを入れたりしました。 しかしエラーが20日に一度ぐらいしか起きないのでなかなか進展しません。

エラーの再現頻度を上げる

(試行錯誤があったのですが省略)その後8月の中頃に、データの読み書き頻度を調整することでエラーの再現頻度をあげるチェッカーツールwldev-checkerを作ります。

wldev-checkerを用いると最短で10分に一度でエラーが発生するようになり、現象を追跡しやすくなりました。 ただ数時間走らせてもエラーがでないことがあります。 現象の違いを調べることでデータflush周りに何か問題がある可能性が高くなりました。

再度WalB driverを調べたところキャッシュ制御に関わるフラグREQ_FLUSHとREQ_FUA(Explicit volatile write back cache control)の扱いにミスがあったことに気づきます。 そのあたりを修正することでwldev-checkerでもエラーがでなくなりました。

問題解決…と思ったら

これで解決したかと思われたのですが、数日後またもやエラーが報告されます。 ただ今度はチェッカーツールを用いても約3日に一度しか再現しません。 diskへの書き込みと同時にメモリにも出力する仮想ブロックデバイスを作成したり、driverのチェックコードを強化したりしますがなかなか特定にいたりません。

原因追求が難しい理由の一つにdisk読み書きの非同期性にあります。 ユーザランドでのwriteシステムコール発行から実際にdiskにデータが書き込まれてflushされ、そのデータを読み込んでチェックするまでたくさんのworkerキューを通ります。 したがってエラー検出した時点のバックトレースはほとんど役に立たないのです。 またdriver内でログを記録しようとするとそのIOが本来のwrite IOを邪魔してしまいます。

それからdriver内部で一つのデータが複数に分割されたり、その逆に複数のデータが一つにまとめられたりすることもあります。 そうなるとデータの追跡が難しくなります。 実際、writeされたデータの一部だけが壊れるという現象が発生していました。

そんな中、10月頃kernelをハングアップさせてrebootする作業をしているときにシステムをcold bootすると再現性が高くなることに気づきます(ソフトウェアrebootでは発生しない)。 このあたりでソフトのバグではなく、ハードのバグの可能性も考慮し始めました。

もちろん当初から複数の環境でテストすることでバグが再現するマシンとしないマシンがあることには気がついていたのですが、それがハードの差によるタイミングの問題なのか、ハードそのものの問題なのかの切り分けが難しかったのです。 また私たちのツールの細かいバグをいろいろ修正していたので安易に他人のせいにするわけにはいきませんでした。

原因の特定

(試行錯誤があったのですが省略)その後今度はWalB driverを用いずに、WalB driverと似たようなdisk IOを発生させるユーザランドのツールを作成します。 そしてこのツールを用いてもlogpackエラーが発生したのです。 これによりWalBのせいではないことが確定しました。心が晴れた瞬間です!!!

私たちの特殊なdriverを使わないでもバグを再現できたことでユーザランドのみで完結し、ぐっと調査しやすくなりました。 disk IOの詳細を表示できるblktraceを用いて調査を進めると、IOが発行されているのにdiskに書き込まれない証拠が見つかりました。

下記はblktraceの一部を抜粋して成形したものです。

252,7 1 12079 228.808092957 4494 Q W 3296001 + 1 [kworker/u66:21]← logpack header submitted
 8,2 1 162836 228.808094689 4494 A W 622367489 + 1 <- (252,7) 3296001
 8,0 1 162837 228.808095153 4494 A W 622867201 + 1 <- (8,2) 622367489
 8,0 1 162838 228.808095648 4494 Q W 622867201 + 1 [kworker/u66:21]
 8,0 1 162839 228.808099628 4494 G W 622867201 + 1 [kworker/u66:21]
252,7 1 12080 228.808101183 4494 Q W 3296002 + 256 [kworker/u66:21] ← logpack IO submitted
 8,2 1 162840 228.808101724 4494 A W 622367490 + 256 <- (252,7) 3296002
...
(snip)
...
 8,0 1 162854 228.808198793 4494 Q W 622867459 + 256 [kworker/u66:21]
 8,0 1 162855 228.808199268 4494 G W 622867459 + 256 [kworker/u66:21]
 8,0 7 187897 228.808255052 4367 C W 622867202 + 256 [0]
252,7 7 19389 228.808256993 4367 C W 3296002 + 256 [0] ← logpack IO completed
 8,0 7 187898 228.808260877 4367 C W 622867201 + 1 [0]
252,7 7 19390 228.808261198 4367 C W 3296001 + 1 [0]← logpack header completed

データフォーマットの詳細は省略しますがここでは252,7はデバイス番号, 8, 2はsda2を表します。 3296001セクタ目に対して書き込みがrequestキューにsubmitされ(Q)、最後にcompleted(C)が表示されています。 つまりここでdiskへの書き込みは完了しているはずなのです。 しかしそのセクタを読むとzeroのままでデータが書き込まれていませんでした。

これによりdisk*1の不具合であることがほぼ確定です。

詳しい調査の結果、複数同時にデータ書き込みを行うと、その書き込みが完了したのに、そのデータが実は書き込まれずにロストすることがあるというRAIDコントローラの不具合だとわかりました。 信頼性が売りのRAIDコントローラでもそんな基本的なバグがあるのですね。

なお、新しいRAIDコントローラを持つサーバでチェックツールを動作させたところ、その様な不具合は再現しませんでした。

感想

ソフトのバグとハードのバグが微妙に絡んでいたため特定に時間がかかってしまいました。 今回のバグ調査は非常に長期にわたったため(もちろんこれだけをしていたわけではありませんが)、星野は精神的にかなり辛かったようです。 負けない心が大事ですね(cf. 「バグを突き止める技術」)。

今後

この調査によって様々な負荷テストが追加されて、システムの信頼性も高くなったと思われます。 現在は社内環境への導入に向けて進めているところです。


*1ここでdiskと書いていますが、実際には10台のHDDでハードウェアRAID6を組み(+2台のホットスワップ)、その上にLVMで作成した論理ボリュームを指します。 ハードウェアRAID6を使わずにLVMだけで管理したボリュームに対しては不具合が発生しないことは確認済みです(2016/1/13追記)。
参考 : cybozu.com : お客様の大切なデータを守る4重のバックアップ


Turnip + Sinatra + kintone でメンテナブルなE2Eテストを書こう

$
0
0

こんにちは。大阪開発部の岡田(@y_okady)です。大阪のランチは今日も安くて旨いですね。

サイボウズと言えばSeleniumを使ったE2Eテストのイメージが定着しつつありますが、そのE2Eテストをさらに強固なものにするために新しい仕組み作りにチャレンジしました。

作り始めて3日しか経っていないプロトタイプの段階ですが、構成はだいたい固まってそれなりに動くものができたのでご紹介します。

f:id:cybozuinsideout:20160107191720p:plain

きっかけ

2週間ほど前に読んだ「エッセンシャルスクラム」に、こんなことが書いてありました。

テストはスプリント期間よりも長くかかる場合がある。 そうなるのは、開発チームが巨大な手動テストという負債を背負っているからだ。

サイボウズではE2Eテストの自動化が進んでいると言っても、自動化できているのは試験全体のほんの一部です。ほとんどのテストは手動で実施しており、プログラマが1ヶ月かけて開発したものをQAが1ヶ月かけて手動でテストしています。

この巨大な手動テストという負債を無くすことができれば、より良い開発ができるんじゃないか。これはもうやるっきゃない!

要件

プログラマにとってもQAにとってもメンテナブルであること、これが一番の要件です。他にもいろいろあるのですが、ここでは割愛します。

構成要素

Turnip

https://github.com/jnicklas/turnip

Cucumberの後継として期待されているE2Eテストフレームワークです。RSpec内でテストを実行します。 自然言語でシナリオ(テストしたい一連のステップ)を記述します。

QAがステップを組み合わせてシナリオを設計し、プログラマがステップの具体的な処理を実装する、といった役割分担が可能となります。

SitePrism

https://github.com/natritmeyer/site_prism

Seleniumのデザインパターンのひとつであるページオブジェクトパターンを簡単に使えるDSLです。 DRYで再利用しやすいテストを書くことができます。

Turnip Formatter

https://github.com/gongo/turnip_formatter

Turnipの実行結果をきれいに出力してくれるformatterです。美しさは正義です。

YARD::Turnip

https://github.com/takaishi/yard-turnip

Rubyのドキュメント生成ツールYARDが生成するドキュメントに、Turnipのステップを含めるアドオンです。 ソースコードを見なくても実装済みのステップを簡単に確認できます。

Sinatra

http://www.sinatrarb.com/

軽量Webアプリケーションフレームワークです。kintoneからRSpecを呼び出したり、Turnipの実行結果やYARDのドキュメントを閲覧するために使用します。

kintone

https://kintone.cybozu.com/

我らがkintoneです。JavaScriptカスタマイズを利用することで、kintoneに登録されたデータを外部のWebアプリケーションに簡単に送信できます。

プロトタイプができるまで

Gemfile

source "https://rubygems.org"

gem "rspec", "3.3"
gem "turnip", "1.3.1"
gem "site_prism"
gem "turnip_formatter", "0.3.4"
gem "selenium-webdriver"
gem "chromedriver-helper"
gem "yard"
gem "yard-turnip"
gem "sinatra"

Turnip Formatter(0.3.4)がRSpec(3.3)、Turnip(1.3.1)じゃないとちゃんと動かず、ちょっとハマりました。

RSpecとTurnipの設定

RSpecでTurnipを実行するためのオプションと、実行結果をTurnip Formatterで出力するための設定を .rspecに記述します。

-r turnip/rspec
-r turnip_formatter
--format RSpecTurnipFormatter
--out web/public/report.html

spec_helper.rbに、Turnipの各種設定を記述します。今回はとりあえず、ChromeでSeleniumを実行するようにしています。

require"capybara/dsl"require"capybara/rspec"require"turnip"require"turnip/capybara"require"site_prism"Dir.glob("spec/pages/**/*page.rb") { |f| load f, true }

Capybara.default_driver = :seleniumCapybara.run_server = falseCapybara.register_driver :seleniumdo |app|
  Capybara::Selenium::Driver.new(app, browser: :chrome)
end

SinatraアプリからRSpecを実行

SinatraアプリでHTTPリクエストを受け取ってRSpecを実行できるようにします。kintoneからHTTPリクエストを送れるように、HTTPレスポンスヘッダーに Access-Control-Allow-Origin: *を指定しておきます。

require"sinatra"

set :environment, :production

post "/feature"do
  headers \
    "Access-Control-Allow-Origin" => "*"File.write("tmp.feature", params[:feature])
  system("rspec tmp.feature")
end

kintoneからSinatraアプリにHTTPリクエストを送信

シナリオを登録するkintoneアプリを作成して、JavaScriptカスタマイズでシナリオをSinatraアプリに送信できるようにします。

サンプルプログラムは割愛しますが、登録したシナリオをTurnipが解釈できるGherkinという形式に変換して、ボタンクリックで jQuery.postでHTTPリクエストを送信し、処理が完了したら実行結果を別タブで開くだけの簡単なプログラムです。

たとえば、kintoneにログインして非公開スペースを作成し、別のユーザーでログインしてそのスペースが見えていないことを確認するシナリオは、次のように記述できます。

Feature: スペース機能
  Scenario: 非公開スペースを作成する
    * user1 / user1 でログインする
    * [Portal] スペースを作成する
      | スペース名      | 非公開 |
      | サンプルスペース | x     |
    * ポータルアイコンをクリックする
    * "サンプルスペース" と表示されている
    * ログアウトする
    * user2 / user2 でログインする
    * [Portal] "すべてのスペース" のスペース一覧を表示する
    * "サンプルスペース" と表示されていない

なお、GherkinではGiven, When, Thenなどのキーワードを利用するのが一般的ですが、真面目に書くのは大変なので今回は * を使っています。

https://github.com/cucumber/gherkin/blob/master/lib/gherkin/i18n.json#L422-L435

ページオブジェクトとステップの実装

SitePrismを使ってページオブジェクトを実装します。Webアプリケーションのひとつの画面がひとつのクラスになります。 そして、Gherkinで記述されたステップの処理を記述します。

次のサンプルプログラムは、ログイン画面のページオブジェクトと、ログインのステップを記述したものです。 ログイン画面にはユーザー名とパスワードの入力欄と、ログインボタンがあります。 ログインのステップでは、指定されたユーザー名とパスワードを入力欄にセットし、ログインボタンをクリックします。

# ログイン画面のページオブジェクトclassLoginPage< SitePrism::Page
  set_url "<kintoneのURL>"
  element :username, "input[name='username']"
  element :password, "input[name='password']"
  element :submit, "input.login-button"end# ログイン画面のステップ
step ":username / :password でログインする"do |username, password|
  @page = LoginPage.new
  @page.load
  @page.username.set username
  @page.password.set password
  @page.submit.click
end

プログラマは、開発中にページオブジェクトとステップをメンテナンスします。 これらをしっかりメンテナンスし続けることが、E2Eテストにおけるプログラマの役割です。

実装済みステップのドキュメント生成

シナリオを記述するQAが実装済みステップを確認できるようにするために、YARDを使ってドキュメントを生成します。

$ yard -o web/public/doc --plugin yard-turnip **/*page.rb

できたもの

前述の、kintoneにログインして非公開スペースを作成し、別のユーザーでログインしてそのスペースが見えていないことを確認するシナリオを実行してみました。

まとめ

E2Eテストはメンテナンスが大変で、続けるのが難しいという話をよく耳にします。 サイボウズではみんなが歯を食いしばってメンテナンスを続けていますが、やっぱり大変です。 そんな状況の中で、手動でやってるテストを全部自動化したいなんて言ったら袋叩きにされるでしょう。

まだ間に合うかもと思った方、すぐにメンテナブルなE2Eテストを導入してください。

もう間に合わないと思った方、袋叩きを恐れずに導入を検討してください。

メンテナブルなE2Eテストを導入して、高品質なWebアプリケーションをこれからもずっと開発し続けましょう!

ブロックデバイスのクラッシュ模擬ツールcrashblk の紹介

$
0
0

こんにちは、サイボウズ・ラボの星野です。

先日の記事(半年かかったバグ調査の顛末は)では、長期間苦労した不具合調査についてあっさりとまとめて頂いたので、その行間に起きたたくさんのことを思い起こし、ひとり感慨にふけっています。 (私も記事原稿をレビューしましたが、もし私が書いたら思い入れが強すぎて長文になってしまい、きっと読みづらくなってしまったでしょう。。)

さて、今回は、crashblk というソフトウェアをオープンソース化したので、その紹介をしたいと思います。 ソースコードは GitHub レポジトリに置いてあります。 ライセンスは GPLv2 or 3 です。

crashblk とは

crashblk (くらっしゅぶろく、と読んでいます) は、Linux カーネルのブロックデバイスドライバやファイルシステムなどをテストするためのブロックデバイスドライバです。私は WalBを開発していますが、その品質を高めるための道具のひとつとして crashblk を作りました。

crashblk の機能は以下の 3 つです。

  • クラッシュ模擬
  • IO エラー模擬
  • レスポンスタイム設定

クラッシュ模擬は、突然の電源断により書き込みは完了しているものの、永続化されていないデータが消える挙動を、実際の電源断をせずに再現します。IO エラー模擬は、ストレージの故障における振舞いとして、IO が全てエラーになる挙動や書き込み IO だけエラーになる挙動を模擬します。レスポンスタイム設定は、read, write, flush のリクエスト種類毎に、レスポンスタイムの振れ幅を設定することができ、ストレージの様々な性能特性を再現することが出来ます。

crashblk は、永続化保証されたはずのデータが永続化されているか、IO エラー時に期待した挙動をするか、などをテストするために使います。WalB カーネルドライバのテストにおいては、まさにそれらの用途で使いました。また、ファイルシステムのジャーナルによる一貫性制御が正常に動いているかなどのテストにも使えると思います。

実装は、メモリブロックデバイスドライバとなっています。内部的には、アドレスをキーとする rb-tree 構造を 2 つ使って永続化済みでないデータと永続化済みのデータを分けて保持し、永続化済みではないデータを捨てることでクラッシュ模擬を実現しています。また、レスポンスタイムを調整するために、delayed workqueue を利用しています。rb-tree や delayed workqueue は Linux カーネル内の道具として用意されており、WalB カーネルドライバの開発経験もあったので、crashblk を実装するのは比較的簡単でした。

crashblk と不具合調査を巡るドラマ

実は、最初に crashblk を作ったのは 2014 年で、WalB カーネルドライバをテストし、見つかった不具合を修正して、そこで満足していました。しかし、 先日の記事のひとつ目の不具合は、 WalB カーネルドライバの flush (永続化)に関するものだったにも関わらず、crashblk によるテストで発見することが出来ていませんでした。

その理由は、crashblk の IO 処理方法が実在のストレージにおける IO 挙動に比べると単純すぎたことです。一般にストレージデバイスは、IO をスケジューリングし実行順序を並び換えることが許されており、IO が submit されてから、実際に処理を行い、completion を返すまでの時間幅もあります。複数 IO の実行時間は重複し得るのです。

以下の図を見てください。 f:id:cybozuinsideout:20160108174447p:plainWrite Aが完了する前に submit された左側の Flushは、Aを永続化する責任がありませんが、右側の FlushAを永続化する責任があります。件の不具合は、左側の FlushAが永続化されるものだと勘違いしており、実装もそうしていたことが原因でした。実際に評価に使ったストレージはまさにこの違いが出ることのある性能特性を持っていたのです。

それまでの crashblk は IO を直列に実行してしまうので、複数 IO 実行時間が重複するという挙動を模擬できていませんでした。それに関連して起きる不具合も、当然再現することは出来なかったということになります。それがまさに件の不具合だったのです。

先程紹介した 3 番目の「レスポンスタイム設定」機能は、件の不具合を発見できなかった反省から、昨年末に開催された社内ハッカソン(こちらの記事を参照)で、実装しました。この機能を使うことにより、不具合を容易に再現出来ることを確認しました。

以下の図が、ハッカソン前後の IO 処理フローを示したものになります。 f:id:cybozuinsideout:20160108174502p:plainf:id:cybozuinsideout:20160108174512p:plain通常のキューではなく、ランダム時間待ってタスクを実行する delayed queue を利用することで、IO 順序の入れ替えを表現し、複数 IO の重複を模擬できるようになりました。

先日の記事では、件の不具合を含め 2 つの不具合が発見された旨を紹介していますが、実は 3 つ目の不具合が、WalB のユーザーランドツールに存在していました。これら 3 つの不具合は全て invalid logpack という同じ現象として観測されるので、苦労して不具合を直しても、残っている不具合起因で現象が再現する、という悪夢が 2 回も起きたことになります。心が折れそうになって有給休暇を使いまくっても仕方ないですね(^^;

他の 2 つに比べて原因特定までの時間が短かったため、先日の記事では省略されていますが、3 つ目の不具合再現頻度を 3 日に 1 回から 30 分に 1 回と、2 ケタ上昇させることが出来、原因特定までの時間を早めてくれたのは、改良版の crashblk でした。ありがとう、crashblk。頑張った、俺。

まとめ

今回オープンソース化した crashblk の紹介をさせていただきました。これを使いたくなる方は少ないかも知れませんが、興味のある方は是非触ってみてください。比較的単純な作りになっていますので、Linux カーネルのブロックデバイスドライバを作ってみたいという方の勉強用としてもオススメです。


IntelliJ/PHPStormの拡張Pluginの作成 (Git, kintone連携)

$
0
0

こんにちは、Garoon開発チームの杉山です。

いきなりですが、みなさんは IDE を使ってますでしょうか。サイボウズ内では Javer な方々は IntelliJを使っていたり、僕は PHPer なので PHPStormを使っていたりします。

どの IDE にも把握しきれないほど多くの便利な機能が備わっていますが、せっかくなら、自分たちの開発スタイルに合わせてカスタマイズしていきたいものです。

今回は Garoon チームで行っている振り返りから上がった問題を改善する IntelliJ 系の IDE のプラグインを作成しました。

プラグインを PHPStorm でも動くようにしたり、ライブラリを読み込ませたりするのに少しつまづいたところがあるので、その手順をまとめておきたいと思います。

何を作ったか

Garoon チームでは開発タスクや不具合を kintoneアプリで管理しています。ソースコードは GitHub で管理しているのですが、Git の運用ルールもあり、ブランチ名はレコード番号に、コミットコメントにもタスク・不具合のレコード番号を付けるようにしています。

この運用によって、各コードがいつ、どんなタスクで追加されたものなのかが調査しやすくなりました。

しかし、現状ではコミットメッセージを毎回手動で入力する必要があり、レコード番号を書き間違えてしまう問題も頻発していました。そもそも、タスク番号を手動で入力するのは面倒です。ブランチ名とコミットメッセージにつけるタスク番号が同じなら、それくらいは自動でつくようにして、ついでにそのタスクの情報も kintone からとってくるようにしました。

完成イメージはこんな感じです。 f:id:cybozuinsideout:20160119120731j:plain画像は PHPStorm でコミットするときに出てくるパネルです。赤丸の Garoon のアイコンを押すとコミットメッセージが変化して、kintone からタスクのタイトルを取ってきて吹き出しで表示するようにしました。

作成方法

1. IntelliJ IDEA のインストール

PHPStorm のプラグインを作るときに、初めに襲い掛かるハードルは IntelliJ を使わないといけないことだと思っています。言語は当然 Java になるので、Java 初心者な私にはいささかきついところもありました。

ちなみにこの記事の手順で、JetBrains から出ている IDE であれば、どれでも使えるプラグインの作成が可能です。

Ultimate Edition (有料版) を使いたいところでしたが、Community Edition (無償版) で我慢しました。プラグイン作成はほぼ問題なく行えました。

2. Project を作成する

  • [File] -> [New] -> [Project] とクリック
  • 左のリストから [IntelliJ Platform Plugin] を選択する
  • プロジェクト名を入れて [Finish] を押す

f:id:cybozuinsideout:20160119133617p:plain

3. plugin.XMLを編集する

プラグインの基本情報は、"resources\META-INF\plugin.xml"に記述します。主にプラグインのインストール画面に表示される情報や、ライブラリの依存関係を記述します。

また、IntelliJ 以外の IDE(PHPStormとかRubyMineとか)でも動作するプラグインを作成する場合は、ファイル中ほどにある

<depends>com.intellij.modules.lang</depends>

を有効にする必要があります。

4. アクションを作成する

プラグイン内ではアクションと呼ばれる動作単位を記述していきます。アクションは次の手順で作成します。

  • srcフォルダを右クリック
  • [New] -> [Action]
  • Action ID, Class Name, Name を入力
  • Groupsを選択
  • [OK]ボタンを押す f:id:cybozuinsideout:20160119160137p:plain

Action ID とクラス名は基本的には同じもので大丈夫そうです。Name はプラグイン使用時に見えることがあるものなので、簡単な説明文とかを入れてもいいかもしれません。

アクション作成時に重要なのは手順4の Groups の選択です。対応する Groups さえ見つけられれば、各ツールバーやメニューの中にアクションを作成できます。グループを選択するとそのグループに属しているアクションの一覧が表示されるので、それを見ながらお好みのグループを探すとよいと思います。

今回作ったプラグインは VCS(Version Control System) のコミットメッセージに関するものだったので、"Vcs.MessageActionGroup"グループにしました。

上記の手順で actionPerformedメソッドを持つクラスが作成されます。イベント発火時に actionPerformedメソッドが実行されます。

また、plugin.xml を見てみると、

<action id="SetTaskName"class="SetTaskName"text="Set Task Name."><add-to-group group-id="Vcs.MessageActionGroup"anchor="first"/></action>

と、アクションに関する記述が追加されます。

アイコンを表示するには、画像を用意して action タグ に icon 属性を足せばオッケーです。

<action id="SetTaskName"class="SetTaskName"text="Set Task Name."icon="garoon.png"><add-to-group group-id="Vcs.MessageActionGroup"anchor="first"/></action>

5. 動かしてみる

実装では IntelliJ IDEA's Open APIを使っていきます。詳しい Reference はまだ乏しいので、使いかたを調べるのには公開されているプラグインのコードを見に行ったほうが早かったりします。

とりあえず動くかどうかを確認するためにこんなコードを書きました。

publicvoid actionPerformed(AnActionEvent e) {
    BalloonBuilder balloonBuilder = JBPopupFactory.getInstance().createHtmlTextBalloonBuilder("Hello World!!", MessageType.INFO, null);
    final Balloon balloon = balloonBuilder.createBalloon();
    balloon.show(new RelativePoint( new Point(0, 0)), Balloon.Position.above);
}

"Hello World!!"って Balloon が表示されるだけのコードです。

デバッグには設定が必要ですが、

  • [Run] -> [Edit Configurations...]
  • プラスマークからPluginを追加
  • "Use classpath of module"を設定

で大丈夫です。これ以降[Run]なり[Debug]なりを押すと新しく IntelliJ が起動して実装したプラグインを試すことができます。

これで[VCS] -> [Commit Changes...]と行くとパネルの真ん中あたりに Garoon のアイコンが表示されて、クリックするとメッセージが表示されるようになりました! f:id:cybozuinsideout:20160119181914p:plain

ブレークポイントとかも設定できるのでデバッグはわりと楽でした。

残念なのは、PHPStorm を利用してデバッグできないこと。IntelliJ も PHPStorm も動作はほとんど同じなので問題ないといえばないのですが、PHPStorm に特化した拡張プラグインの作成は少し難しそうです。

ライブラリの追加

今回は Git の情報を取得する必要があります。調べてみると、git4ideaというライブラリが使えそうなのでインクルードします。
git4idea.jar は(IntelliJのInstallディレクトリ)/plugin/git4idea/lib/ に存在します。

  • [File] -> [Project Structure]
  • [Modules] -> [Dependencies]
  • プラスマークから、 git4idea.jar を選択、追加
  • Scope を"Provided"に変更する
  • plugin.xml に<depends>Git4Idea</depends>を追加する

今いるブランチ名をとってくるのはこんな感じでできました。

Project project = e.getProject();
if (project == null)
    return;
VirtualFile virtualFile = project.getBaseDir();

GitRepositoryManager gitRepositoryManager = GitUtil.getRepositoryManager(project); 
GitRepository gitRepository = gitRepositoryManager.getRepositoryForFile(virtualFile);
if (gitRepository == null)
    return;
GitBranch gitBranch = gitRepository.getCurrentBranch();
if (gitBranch == null)
    return;

String branchName = gitBranch.getName()

また kintone と連携するのには kintone API SDK(β) for Javaを使いました。
この SDK のインクルード手順は、

  • 上記サイトからプログラムをダウンロード
  • GitHub の Readme の Build .jar file に従って jar ファイルを作成
  • [File] -> [Project Structure]
  • [Modules] -> [Dependencies]
  • プラスマークから、作成した jar ファイルを選択、追加
  • Scope を"Compile"に変更する

とすれば大丈夫です。 git4idea の時とは Scope が違うのではまりました。詳細は IntelliJ IDEA のヘルプページをご覧ください。

kintone API SDK では、レコードのタイトルが欲しいだけなので、API トークンを使いました。実際のコードはこんな感じです。

Connection connection = new Connection("domain", apitoken);
try {
    String response = connection.request("GET", "record.json?app="+appId+"&id="+recordNumber, "");
  //以下JSONの処理
} catch (DBException error) {
    //以下エラー処理    
}

6. PHPStorm で動かす

デバッグである程度動作の確認ができたら、PHPStorm でも動作を確認しましょう。PHPStorm で確認するには、一度 jar ファイル(またはzipファイル)をビルドする必要があります。
[Build] -> [Prepare Plugin Module "Hello Plugin" For Deployment]("Hello Plugin"にはプロジェクト名が入ります。)を押せばビルドができます。

生成されたら

  • PHPStorm を開く
  • [File] -> [Settings...]
  • [Plugins]を選択
  • 画面中央にある[Install plugin from disk...]をクリック
  • ビルドした jar ファイルを選択
  • PHPStorm を再起動

とするとプラグインが動くようになります!

社内でほかの人に配布するときも jar ファイルを渡すだけでいいので簡単です。

まとめ

プログラマーが最もお世話になる製品 IDE。自分たちのプロジェクトに合わせて、少しでも使いにくい点や改善できる点があったら、自前でプラグインを作るチャンスです。

公式ページのコンテンツも足りてない部分もありますが、最近は更新が活発です。

みなさまもぜひプラグイン開発でよりよい開発ライフをお送りください!(^-^)/~~

社内で手軽にHTTPSを使いたい!kintone × nginxで圧倒的生産性向上!

$
0
0

こんにちは。生産性向上チームの宮田(@miyajan)です。

サイボウズには、生産性向上チームという、組織横断で使える開発基盤を整備するためのチームが存在します。「チーム」となっていますが、今のところ私一人です。

今回は、弊社製品のkintoneとオープンソースのnginxによるリバースプロクシを組み合わせて、社内のエンジニアが手軽にHTTPSを使えるようにした話を書きます。

モチベーション

開発者が手元の開発環境でHTTPSを使いたい場面というのは、意外とたくさんあります。以下は、実際に社内であったケースです。

  • HTTPSでのみ発生する問題を手元で再現させたい
  • 社内システムをHTTPSで運用する必要がある
    • 例1) SAML認証でSSOするためにHTTPSが必須
    • 例2) Service Workerを利用するためにHTTPSが必須
    • 例3) 社内でDocker Registryを運用しているが、HTTPだと利用者全員に--insecure-registryというオプションを設定してもらう必要がある
    • 例4) iOSアプリ開発でHTTPSによる接続を要求される

自分で証明書を作成する、いわゆるオレオレ証明書で運用する手もあるのですが、信頼されないオレオレ証明書では警告や例外設定といった問題が発生しがちです。また、開発者が個別にそういった作業を行うのは手間なので、もっと手軽にHTTPSを使えるようにしたいと考えました。

そこで、ドメインとワイルドカード証明書を会社で購入し、社内の開発者が手軽にHTTPSを使えるシステムを構築することにしました。

構成

ここからは、開発用のドメインをdev.cybozu.xyzとし、*.dev.cybozu.xyzのワイルドカード証明書を持っているとします。

f:id:cybozuinsideout:20160215150346p:plain

構成としては、上の図のように証明書を配置したリバースプロクシを用意し、クライアントから*.dev.cybozu.xyzへのリクエストはリバースプロクシを経由してサーバーに送られるようにします。クライアント⇔リバースプロクシ間の通信はHTTPSとなりますが、リバースプロクシ⇔サーバー間の通信はHTTPのままで問題ありません。なので、なんらかのサーバーを立てる開発者側はHTTPSまわりの設定はなにもしなくてよくなります。必要なのは、リバースプロクシにドメイン名とIPアドレス+ポート番号の対応を設定するだけです。

f:id:cybozuinsideout:20160215150356p:plain

リバースプロクシの設定が大変だと意味がないので、開発者は上の図のようにkintoneに登録するだけにします。cronで実行されるスクリプトが定期的にkintoneからデータを取得し、DNSとリバースプロクシの設定変更を行います。DNSには登録されたドメインがリバースプロクシを指すように設定し、リバースプロクシには登録されたドメインが指定のアドレスに転送されるように設定します。

kintone

今回のシステムでkintoneの役割は以下になります。

  • データの蓄積
  • データ登録用フォームをGUIで提供
  • 他システムがデータを取得するためのAPIを提供

これらの部分を自作するとなると、かなりの工数が必要となってしまいます。このような連携込みの基幹システムがお手軽簡単に構築できてしまうところが、kintoneの素晴らしい点です(ステマ)。

kintoneだと、以下のようなフォームを持つ業務アプリが、慣れれば3分ほどで作成できます。

f:id:cybozuinsideout:20160215154239p:plain

DNS

DNSは、PowerDNSを使っています。PowerDNSにはREST APIがあるので、連携しやすいと考えたのが理由です。とはいえ、直接ファイルシステムを書き換えるとかでも正直問題ないので、どのDNSソフトを使っても問題ないでしょう。

DNSがあるマシンではcronで毎分スクリプトが実行され、kintoneからHTTPS化したいドメインを取得してきて、PowerDNSが参照するMySQLにリバースプロクシを指すように設定します。

あとは、社内のDNSサーバーから、dev.cybozu.xyzの名前解決を転送するように設定してもらえばOKです。

リバースプロクシ

リバースプロクシは、nginx+lua-nginx-moduleredisの組み合わせで実現しています。nginxの設定ファイルは以下のようになります。

worker_processes  auto;

# redisの接続先を環境変数から取得
env REDIS_ADDR;
env REDIS_PORT;

events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;

    sendfile        on;
    keepalive_timeout  65;

    server {
        listen       80;
        server_name  *.dev.cybozu.xyz;
        return 301 https://$host$request_uri;
    }

    server {
        listen       443 ssl;
        server_name  *.dev.cybozu.xyz;

        ssl_certificate /etc/nginx/crt/nginx.crt;
        ssl_certificate_key /etc/nginx/crt/nginx.key;

        ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
        ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-RSA-AES256-SHA:AES128-SHA:AES256-SHA:DES-CBC3-SHA;
        ssl_prefer_server_ciphers on;

        add_header        Strict-Transport-Security 'max-age=31536000; includeSubDomains';
        proxy_hide_header Strict-Transport-Security;

        client_max_body_size 0;

        location / {
            set $upstream "";

            # luaでredisからプロクシ先を取得する
            rewrite_by_lua '
                local redis = require "redis"
                local client = redis.connect(os.getenv("REDIS_ADDR"), tonumber(os.getenv("REDIS_PORT")))
                route = client:get(ngx.var.http_host)

                if route ~= nil then
                    ngx.var.upstream = route
                else
                    ngx.log(ngx.ERR, "Host not found: ", ngx.var.http_host)
                    ngx.exit(ngx.HTTP_NOT_FOUND)
                end
            ';

            proxy_set_header    Host $host;
            proxy_set_header    HTTPS on;
            proxy_set_header    Forwarded "proto=https; for=$remote_addr; host=$host";
            proxy_set_header    X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header    X-Forwarded-Proto $scheme;
            proxy_set_header    X-Forwarded-Host $host;
            proxy_set_header    X-Real-IP $remote_addr;
            proxy_set_header    Connection "";

            proxy_redirect      http:// https://;
            proxy_pass          http://$upstream;

            proxy_http_version 1.1;

            proxy_buffering off;
            proxy_request_buffering off;
        }
    }

}

rewrite_by_luaの部分でプロクシ先をredisから取得して動的に設定しています。

nginxがあるマシンでもcronで毎分スクリプトが実行され、kintoneからドメインと対応するプロクシ先を取得してきて、nginxが参照するredisに登録します。

まとめ

社内の開発者が手軽にHTTPSを使えるように、kintoneとnginxで構築したシステムについて説明させていただきました。

このシステムを今年初めに社内公開したところ、現時点で10人以上の開発者に活用していただいています。想定していたよりも社内でHTTPSを利用する機会は多かったようです。

ちなみに、独自CAを運用する方法もありますが、ワイルドカード証明書には証明書発行や利用者のブラウザに証明書を登録する手間が不要であるメリットがあります。

このように、生産性向上チームでは、社内の開発者が本質的な開発に集中できるようにするための活動を行っています。あまり一般的な職種ではないかもしれませんが、組織を横断して生産性を向上させることにより、よりお客様への価値提供を促進していくことがミッションです。

今回この記事を書いたことによって世の中の生産性向上へもつながれば幸いです。

React.js meetup #3 を開催しました

$
0
0

こんにちは、kintoneプログラマの前田です。

2月23日(火)サイボウズ東京オフィスで React.js meetup #3が開催されました。今回はこのイベントの各セッションの内容を簡単にご紹介します。なお、当日の様子は togetterでもまとめられています。

React.js meetup とは

その名の通り React についての meetup で、今回は3回目になります。当日は80人を超える参加者で大変盛り上がり、React の注目度が感じられる meetup でした。 f:id:cybozuinsideout:20160302110754j:plain

Evolving Complex Systems Incrementally

最初は @cpojerさんによる発表から始まりました。JavaScript をモダンなコードに変換するための jscodeshiftを紹介する話だったと思います。全編英語で内容をすべて把握できたわけじゃないのですが、書き方が変わるとこのようにツールをつかってガシガシ既存コード書き換えていくという話が印象的でした。こうやって日々進歩する JavaScript にキャッチアップしてるんですね。 f:id:cybozuinsideout:20160303093833j:plainf:id:cybozuinsideout:20160302180601j:plain

Missing Pages: React/Flux/GraphQL/RelayJS

2番目の発表は @neth_6さんです。こちらも全編英語の発表でした。内容は GraphQL と Relay についてです。

QraphQL の説明や、React コンポーネントの再利用性をもっと進めるための Relay の導入等、大変分かりやすく解説していただきました。

React.js ユーザのための Elm

3番目は @jinjorさんによる React.js ユーザのための Elm でした。 Elmは静的型付けの関数型言語で、ソースコードは JavaScript のコードに変換される、FRP (Functional Reactive Programing) の言語です。最近では Virtual DOM が導入されたようです。

発表の後半で Elm Architecture について紹介していただきました。Elm は FRP の言語だからといって、何も考えずに実装するとコードがカオスになってしまいがちなので、何らかのうまい設計が必要です。そこで Elm では Elm Architecture というのが考えられていて、これを使うとシンプルに記述できるようです。

React でウィジェットライブラリを作った話

4番目の発表は @yosuke_furukawaさんです。ウィジェットライブラリを React で実装した時のお話しでした。

詳しく解説してくださったのは、css-modules を用いて JavaScript の中に CSS を埋め込んだ話です。ライブラリが JavaScript ファイルだけとなったのはメリットですが、script を書く位置が難しい等デメリットもいくつかあるようです。

LT: Ruby の Virtual DOM 実装 'Hyalite'について

最初の LT は Ruby の Virtual DOM 実装 'Hyalite'についてで、@youchanさんの発表です。Hyaliteは Ruby の Virtual DOM 実装で、jsx の代わりの便利なメソッドも定義されているみたいです。

LT: React コンポーネント設計と CSS

2番目は React コンポーネント設計と CSS で、hokumaさんの発表です。既存システムの画面を React で書き直した時のお話しを紹介されました。

画面上の Feed や Post などはそのまま React の Component にしてしまえばいいのですが、例えば Button はどうするのかが問題に上がっていました。最初は素朴にタグを直書きされていたみたいですが、ボタンの実装にばらつきがでたり、共通の処理を差し込みたい場合に手間がかかる等の理由で Button コンポーネントをつくられたみたいです。css はコンポーネントの階層に合わせて階層化され、特に問題はないようです。

LT: React と Loopback ことはじめ

3番目は @tyshgcさんの、React と Loopback ことはじめという発表でした。

Loopback はデータモデルの定義から自動で REST API を生成してくれるツールです。モデルのモックデータは非 PG でもさくっと登録できるようになっているみたいで、さらに React-Loopback を使えば簡単に React と連携できるとのことで、プロトタイプをさくっと作るにはよさそうなツールでした。

LT: React/Reduxにおける現場での喜びとツラみ

LT最後は ryota kaneko さんの React/Redux における現場での喜びとツラみでした。

が、喜びはもはや言うまでもないということでツラみの発表でした。まだまだ発展中の ES6 Modules やトランスパイラの仕様変更によってユニットテストが動かなくなるという事例などを紹介していただきました。デグレを防ぐことやインタフェース設計がうまくできているか等を考慮するためのテストが動かないのはツラいです。

おわりに

前述した通り今回の React.js meetup は80人を超える参加者となり、React の盛り上がりが感じられるイベントとなりました。このような交流を通して React の知見やノウハウがたまり、みんなで楽しい JS ライフが送られるようになればと思います。 f:id:cybozuinsideout:20160302113345j:plain

Viewing all 694 articles
Browse latest View live