Raccoon Tech Blog [株式会社ラクーン 技術戦略部ブログ]

株式会社ラクーン 技術戦略部より、tipsやノウハウなど技術的な話題を発信いたします。

開発

現場のシステム開発を体験できる!開発インターンをレポートします

ラクーンのエンジニア石川です。
グループ内の売掛保証システムを担当しています。

弊社では様々なインターンシッププログラムを実施しており、その一環で技術戦略部も開発体験のプログラムを用意しています。
開発体験とはいいつつ内容の幅は広く、ビジネスモデルに関する研修から始まり、データセンター見学や先輩エンジニアとの交流など、様々な経験ができます。

今回は私がメンターを担当した2016年3月に行われたインターン研修についてご紹介します 。

▼メンター石川 / インターン生の尾碕さん / メンター阿部
c34b4d9d

どんなことをするのか?

今回インターン生に開発してもらうのは、「売掛保証システムの会員登録画面に新規項目を追加」という案件です。
なんと、実際に本番稼働しているサービスの開発案件を担当してもらいインターン期間中にリリースまで行います。

開発期間が短い上に不具合も許されないのでインターン生も、私たちメンター側も真剣に取り組む必要があり正に生の開発現場を体験できる研修になっています。
また、インターネットで公開されているサービスから案件を選んでいるため、インターン期間後に学校の仲間や家族に見てもらうこともできて、より大きな達成感を感じてもらうことができるんじゃないかと思います。

▼インターンシップのカリキュラム
1日目 : ビジネスモデル研修・システム構成講義 
2日目 : システム開発体験 
3日目 : データセンター見学・自由時間 
4日目 : システム開発体験 
5日目 : リリース・発表・打ち上げ

1日目 : ビジネスモデル研修・システム構成講義

ビジネスモデル研修 

この研修では、弊社代表の小方がラクーンで行っているサービスについて説明します。
後で感想を聞いたところ、普段は馴染みのないビジネスモデルという概念が新鮮で非常に興味深かったようです。

システム構成講義

業務システムの開発にはプログラミングスキルだけでなく、様々な経験と知識が必要です。
そこで、まずは座学で一般的なシステム開発の流れを学んでもらいます。

続いて、携わってもらう売掛保証サイトの紹介と、今回追加したい機能を説明します。
サービスに対して、どんな意味がある機能をどんな流れで実装してリリースするのかを理解してもらいます。
 
▼携わることになるサービスやシステムの説明
写真 (1)

説明が終わったら、次はいよいよ売掛保証サービスの担当者に具体的なヒアリングを行います。

サービス担当者へのヒアリング

ヒアリングでは担当者の頭の中にあるイメージをどれだけ具体化できるのかが重要です。
完成した後に「イメージしていたものと違う!」と、ならないようにお互いの齟齬をなくします。

ヒアリングした要件は下記のようなものでした。
  • 項目名は連絡事項とする
  • 連絡事項を会員登録画面に追加して、入力されたデータは社内の管理画面から閲覧できる
  • 用途を限定せずに汎用的な目的に使えるように自由な文字列で入力できる
  • 入力必須ではない
  • 文字数制限を100文字とする
ヒアリングして分かったのですが、現在は連絡事項の入力ができないので申し込み前後に電話でのやりとりが発生するケースがあるとのこと。また予想外の使い方として、営業担当者が出先でお客様からの依頼を受けて代理登録する際に、社内担当者への業務連絡などにも利用できるようです。

▼ヒアリング時に作成した画面モック
モック

ヒアリングではインターン生も積極的に意見を述べ議論に加わっていました。
現場の意見を聞きながら仕様を策定していく場に参加できたことは貴重な体験になったのではないかと思います。 

2日目 : 開発

2日目、いよいよ開発がスタートします。

事前に用意しておいた開発環境一式で作業にはいります。
使い方の説明が終わったら開発環境上で実際のプログラムを動かしてみます。
ソースコードを自由に書き換えながら動かすことで、フレームワークやシステム構成をより深く理解できたようです。

▼インターン生への説明風景、弊社エンジニアは全員2~3枚のディスプレイをデスクに並べています
F1000332

▼最終日に行われたインターン生の発表資料から抜粋。システムをよく理解していることが伺えます。
MVC

3日目 : データーセンター見学・東京見学

3日目はいったんシステム開発をお休みして、午前中にデータセンター見学に行きました。
元々インフラにも興味があったらしく、普段目にすることのない無機質な空間が新鮮だったようです。

そして午後は自由時間です。今回のインターン生は首都県外から来てくれていたので、東京観光を楽しんだようです。
翌日どこに行ったのかを聞いてみたら、かねてから興味のあった秋葉原のボードゲーム専門店に行ってきたのだとか?その実力の程は5日目の打ち上げで発揮されることとなります。
 

4日目 : 開発・テスト

4日目は開発の続きです。今回の案件では、データベースへのカラム追加が必要です。
データベースを使ったことはなかったようですが、簡単な説明をしたら、あとは自分で調べてSQLを使いこなしていたので驚きました。

システムテスト

一通りの開発が完了したので、機能として問題ないかテストを行います。
弊社で利用しているフォーマットに合わせてテスト仕様書を作成してもらいます。
メンター側でテスト仕様書をチェック、足りないテスト項目を追加してもらい、求められている要求に即しているか、プログラムは問題なく動いているか、などをテストしてもらいました。
実装に問題ないことが確認できたら、今度はユーザ部門の担当者に確認してもらいます。
担当者チェックも問題なかったので、あとは5日目のリリースを待つばかりです。

5日目 : リリース・発表・打ち上げ

5日目、最終日です。

リリース

最後の作業となるリリースですが、本番環境の権限の関係で、メンターが作業するのを横で見ていてもらいました。

リリース前に連絡事項がない状態で会員登録フォームを画面に表示してもらいます。
そして、リリース作業の完了後に開いているページを更新してもらった結果、連絡事項が表示されました。
無事リリース完了です!

▼連絡事項が追加されました完成
一通りの作業を完了しました、期間も短く大変だった分、達成感も大きかったと思います。

発表会

インターンの集大成となる発表会です。
事前にメンターと行ったリハーサルでは、上手く話せるか不安がっていましたが、最後までしっかりした発表になりました。

▼リラックスしたムードながらしっかりした発表会となりました
F1000328

思いが伝わる良い発表会だったと思います、お疲れさまでした!

打ち上げ

主賓が未成年ということもあり打ち上げはノンアルコールで行いました。
社内のフリースペースを利用してドリンクを持ち寄り乾杯!
メインは人形町今半のすき焼きです。

▼沢山の先輩エンジニアに囲まれて少し緊張気味の滑り出し
F1000335

インターン中は忙しくてなかなかコミュニケーションが取れなかったメンバーからも、労いの言葉や発表会の感想などをもらっていました。

そんな中、嬉しいサプライズが!ボードゲーム好きということを聞きつけた先輩からボードゲームのプレゼントがありました。

▼いい笑顔です
F1000333

▼お馴染み?のレジスタンス:アヴァロン。詳しくは『レジスタンス:アヴァロン』を遊びやすくするDIY
F1000334_

この流れで、ボードゲーム部主催でボードゲーム大会を行いました。
「スカル」というゲームです。
 
▼ボードゲーム 「スカル」
F1000336

初めてのゲームだったようですが、似たゲームをやったことがあったのか、なんと!優勝していました。

開発実習で疲れきった頭を休めるはずが、よりヒートアップした感もありますが・・・、大変お疲れさまでした。

インターン生の感想まとめ

開発にあたって工夫した点・苦労した点
  • MVCモデルを教えて貰った後、そのソースコードを詳しく見て、次回に変更する点や、編集するファイルを考えていました。
  • MySQLやOracleなどのDBは今まで触れたことがなかったため、操作に苦労しました。
  • 単体テストの時に、一部のブラウザでは問題なく動作したはずが別のブラウザだと動作しない不具合が起こってしまい、様々な動作環境でテストすることの大切さを学びました。
システム開発体験をして
  • 実際に社内の雰囲気を感じながら社員の方と同じように作業したのは、とてもいい経験になりました。
  • 自分が作ったものが社会に出ていることが実感できるのは強いやりがいを感じました。
  • なぜ「このようにしたのか」「別の方法の方がいいのではないか」と疑問に思ったことを聞くと、「このようにした理由」「開発するにあたって考えるといい点」など、授業では学べないことを知ることができました。
5日間を通して

今回私は、5日間でのシステム開発の体験をさせて頂きました。
社員さんと一緒の現場で開発や、データセンターの見学、社員の方の経験談を聞くなど様々な経験を得ることができました。
今回の経験を、これから就職活動などで是非活かしたいです。
 

最後に

ラクーンにおける開発インターンの良い所をご紹介!

実戦経験
実際に業務に参加してもらうことで、自分が学んでいる技術が社会でどんなふうに利用されているのかわかる。

スキルアップ
現場で活躍する先輩エンジニアから、実践で役立つノウハウを学ぶことができます。
 
職場の雰囲気を体感できる
周囲にいる社員の発言や動き、時間の過ごし方なども観察することができ、職場の雰囲気を肌で感じることができます!

以上、ラクーン技術部のインターンシップの紹介でした。 

春なのでSpringへの移行ガイド

こんにちは、開発ユニットbcです。

みなさんご存知かとは思いますが、Seasar2のサポートが2016年9月26日を以って終了します。
弊社の運営するSUPERDELIVERYでも一部の機能でSeasar2を現在も利用しています。

今回はSeasar2からの移行を検討している方の参考になればと思い、以前に行った移行の事例を紹介させていただきます。

2014年にSUPERDELIVERYの商品登録機能をリニューアルした際、弊社ではSeasar2からSpring Framework(4系)へ移行することを選択しました。
選択の理由はプロダクトの開発が停滞していないことと、Seasar2からの移行のしやすさでした。
なぜ移行がしやすいかというと、Seasar2の各機能を代替する機能がSpring Frameworkにも備わっているからです。

ではまずはSeasar2とSpring Frameworkの構成の比較から。

構成の比較

移行前 移行後
Controller S2Struts Spring MVC
DI Seasar2 Spring Framework
DAO S2Dao Spring Data

このようにSeasar2の機能の移行先がSpring Frameworkにもあることが分かります。
では、商品一覧表示の機能を例にしてどのように移行するかを見ていきましょう。

移行前と移行後の実装を比較

移行前のシステムでは、コンポーネント毎に設定ファイルへ記述するスタイルで開発していたので、
新たに機能の追加がある場合は以下のファイルの追加が必要でした。
  1. XXXAction.java
  2. XXXActionImpl.java
  3. XXXService.java
  4. XXXServiceImpl.java
  5. XXXDao.java
  6. XXX.dicon
  7. struts-config-XXX.xml
■ProductSearchAction.java
public interface ProductSearchAction {
  String execute();
}
■ProductSearchActionImpl.java
public class ProductSearchActionImpl implements ProductSearchAction {
  private ProductSearchService productSearchService;
  private List<Product> products;
  private ProductSearchForm searchForm;
	
  public String execute() {
  products = productSearchService.findProducts(searchForm.createProductSearchCondition());
  return "success";
  }

  public List<Product> getProducts() {
  return products;
  }

  public void setProductSearchService(ProductSearchService productSearchService) {
  this.productSearchService = productSearchService;
  }

  public void setSearchForm(ProductSearchForm searchForm) {
  this.searchForm = searchForm;
  }
}
■ProductSearchService.java
public interface ProductSearchService {
List<Product> findProducts(ProductSearchCondition condition);
}
■ProductSearchServiceImpl.java
public class ProductSearchServiceImpl {
private ProductDao productDao;

public List<Product> findProducts(ProductSearchCondition condition) {
return productDao.findByStatusCode(condition.getStatusCode());
}

public void setProductDao(ProductDao productDao) {
this.productDao = productDao;
}
}
■ProductDao.java
public interface ProductDAO {
  @Query("status_code = ?")
  List<Product> findByStatusCode(Integer statusCode);
  void insert(Product product);
  void update(Product product);
  void delete(Product product);
}
■action.dicon
<components>
... <component name="productSearchAction" class="jp.ne.raccoon.action.product.impl.ProductSearchActionImpl" instance="request"> </component>
... </components>
■service.dicon
<components>
  ...
  <component
    class="jp.ne.raccoon.service.product.impl.ProductSearchServiceImpl">
  </component>
  ...
</components>
■dao.dicon
<components>
  ...
  <component
    class="jp.ne.raccoon.dao.ProducDao">
  </component>
  ...
</components>
■struts-config-product.xml
<struts-config>
  <action-mappings>
... <action path="/product/search" type="jp.ne.raccoon.action.product.ProductSearchAction" parameter="execute"> <forward name="success" path="/product/search.html" /> </action>
... </action-mappings> </struts-config>
  1. DIはdiconファイルにコンポーネントを個別に定義して、Seasar2のセッターインジェクションを利用していました。
  2. リクエストのマッピングは、struts-config.xmlにaction-mappingsをaction毎に個別に設定していました。
 
移行後は以下のような実装になりました。
■ProductSearchController.java
@Controller
public class ProductSearchController {
@Autowired private ProductSearchService productSearchService; @RequestMapping(value = "/search", method = RequestMethod.GET) public String execute( @Valid @ModelAttribute("searchForm") ProductSearchForm searchForm, BindingResult bindingResult, Model model) {
model.addAttribute(
"products",
productSearchService.findProducts(searchForm.createProductSearchCondition()));
return "product/search"; } }
■ProductSearchServiceImpl.java (※interfaceは移行前と同じなので割愛してます。)
@Service
public class ProductSearchServiceImpl implements ProductSearchService {
@Autowired private ProductRepository productRepository; public String findProducts(ProductSearchCondition condition) { return productRepository.findByStatusCode(condition.getStatusCode()); } }
■ProductRepository.java
public interface ProductRepository extends JpaRepository<Product, Long> {
  List<Product> findByStatusCode(Integer statusCode);
}

移行前と比べると以下のように実装を置き換えることができました。
Action → Controller
Service → Service
Dao → Repository
diconファイル → @Controller @Service等のアノテーション
struts-config → @RequstMapping
  1. 設定ファイルを必要とせず、Springのアノテーションで コンポーネントの定義とDIを行うようになりました。
  2.  @RequestMappingを利用することでリクエストメソッドの指定やパラメーターの受け取り方等がより柔軟な設定を書けるようになりました。
  3. Daoのinsert/update/deleteのメソッドは、継承元に定義されているのでメソッドを用意する必要はなく、@Queryに相当する物はfindByXXXの部分の命名規則によってwhere句が生成されます。※where status_code = ? が生成されます。
これだけで、新機能の追加に必要な作業がだいぶ軽減されました。
順調に移行できそうでしたが、問題点もありました。

問題点

一つ目が、sqlファイルの外部化です。
S2Daoの資産として、参照系の動的なsqlは外部ファイル化していました。 
Springのプロダクトでは対応している物が見当たらず、可能ならば慣れ親しんだ形で移行したいという思いもあって、Mirageというライブラリを導入することにしました。 

■ProductDao_findProducts.sql
//日付の範囲指定と商品の状態を検索条件で指定できることを例としてます。
select
  product_code
from
  product
/*BEGIN*/ where /*IF dateFrom != null*/ and created_at >= /*dateFrom*/ /*END*/ /*IF dateTo != null*/ and created_at < /*dateTo*/ + 1 /*END*/ /*IF productStatus !=null*/ and product_status = /*productStatus*/
 /*END*/
/*END*/
■ProductDao.java
@Component
@Transactional(readOnly = true)
public class ProductDaoImpl implements ProductDao {
  @Autowired
  private SqlManager sqlManager;

  public Product findProducts(SearchCondition condition) {
//上記sqlファイルを指定 SqlResource sqlResource = new ClasspathSqlResource("ProductDao_find_products.sql"); return sqlManager.getResultList(Product.class, sqlResource, condition); } }
mirageで実装したDaoも@ComponentでSpringのコンポーネントとして定義しました。
必要に応じてspringDataとmirageを使い分けるようにして、移行前の資産も活かすことができました。

二つ目がSeasar2とSpringでのコンポーネントのライフサイクルの違いでした。
S2Strutsの時は、Actionはリクエスト単位で作成していましたが、SpringMVCのControllerでは、デフォルトがシングルトンなため、リクエストやセッション等の短いライフサイクルのコンポーネントのDIができないということでした。
@Controller
@Scope(value = WebApplicationContext.SCOPE_REQUEST) public class XXXController { }
@Scopeの設定でライフサイクルを変えることで対応はできますが、全部のControllerに書くのは・・・ということで、以下のようなアノテーションを作成してライフサイクルをリクエストにしたいControllerに指定するようにしました。
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
@Controller
@Scope(value = WebApplicationContext.SCOPE_REQUEST)
public @interface RequestScopeController {
}
利用する側のController
@RequestScopeController
public class XXXController {
}
これで、リクエストやセッションのライフサイクルのコンポーネントをDIしても問題なく動作するようになりました。

まとめ  

局所的ではありますが、以上がSeasar2から移行した時の内容です。いかがだったでしょうか。
Seasar2のサポート終了をきっかけに新しいことに挑戦していきたいですね。

[E2Eテスト]メンテナンス自動化~パスワードの定期変更に対応する~

こんにちは。
技術戦略部のやすだです。

以前弊社のCI/CDへの取組みについての記事がありましたが、
今回の記事はその中で触れられていた「Selenium」を利用したE2Eテストのシナリオメンテナンスについてです。

経緯 

 弊社ではSeleniumを使ったE2Eテストを行っています。
 その中で、ログイン後の機能について確認するシナリオがあり、ログインに利用するテスト用アカウントのパスワードをシナリオに直接記述していました。

 しかし、弊社ではセキュリティの観点から、社内で利用している全てのテストアカウント用のパスワードをバッチで定期的に自動変更しています。
 
 シナリオに記述されているパスワードについては、定期変更がされる度に人力で変更していたため、シナリオ側のパスワードを変更しそびれると、ログインできずにシナリオの実行に失敗することになってしまいます。 
 というわけで、今回の要望です。
 

要望

 パスワードが変更されたタイミングでSeleniumシナリオに記述しているパスワードも自動で変更したい!
 

概要

 まずはざっくりとパスワード更新バッチとSeleniumのシナリオについての図です。
スライド2

 DBに保存されるパスワードはハッシュ化したものですので、Seleniumのシナリオで利用するパスワードは生成した直後の平文である必要があります。
 そのため、下記の2点の対応を行います。
  1.[パスワード更新バッチ]ハッシュ化前のパスワードをファイルに出力する
   Seleniumのシナリオでログイン後ページを確認するためだけのアカウントなので、何か特別なことができるわけではありませんが、 平文のパスワードを保存するのでサーバーのアクセスコントロールは適切に設定する必要があります。
  2.[テストシナリオ]ファイルからパスワードを取得する

 上記の対応を行うことで、このようになります。 
指摘対応中_技術ブログネタ

詳細内容

 ということで詳細内容です。
 1.[パスワード更新バッチ]ハッシュ化前のパスワードをファイルに出力する
   生成した直後のハッシュ化前の平文パスワードをファイルに出力します。
   Seleinumのシナリオ内では、データファイルとしてHTMLファイルが扱いやすくなっています。
   文字列をHTMLで出力するだけで特別なことは行っていないため、内容については割愛しちゃいましょう。
   ファイルは下記のような形にしています。
password.html
<html>
  <head></head>
    <body>
       <div id="password">1234</div>
    </body>
</html>

 2.[テストシナリオ]ファイルからパスワードを取得する
   Seleniumのシナリオ内ではjavascriptを記述することができます。
   さらに、storeEvalを使うことで値を保持できるため、異なるページで取得した値を取り扱うことが可能です。
   シナリオ内でのjavascriptの実行については、下記の要領でタグ内に記述することで実行することができます。
<td>
 javascript{"'" + this.page().getCurrentWindow().document.getElementById("password").textContent.toString() + "'";}
</td>

  実際にパスワードを取得する際の記述は下記のようになります。
  • パスワードファイルを開く
<tr>
    <td>open</td>
    <td>社内サーバーに配置したパスワードファイルのpath</td>
    <td></td>
</tr>
  • パスワードを取得し、storeする
<tr>
<td>storeEval</td>
    <td>javascript{"'" +this.page().getCurrentWindow().document.getElementById("password").textContent.toString() + "'";}</td>
    <td>get_pass</td>
</tr>
  • storeしたパスワードを他のページで使う
    storeEvalで格納した値は他のページに遷移した後も保持されます。
    そのため、異なるページでも下記の要領で使いまわすことが可能です。
<tr>
    <td>open</td>
    <td>http://www.superdelivery.com/</td>
    <td></td>
</tr>
<tr>
    <td>type</td>
    <td>name=password</td>
    <td>${get_pass}</td>
</tr>


まとめ

  E2Eテストを自動化する最大のメリットはリグレッションテストのコストや障害の発生を抑えられることだと思っています。
 リグレッションテストのコストや障害の発生を抑えることは、システム変更を容易にすることにつながります。
 システム変更が容易であれば、より良いサービスをスピーディーにお届けすることができます。
 ですが、E2Eテストを自動化する場合、どうしてもシナリオのメンテナンスコストが発生してしまうのが悩みどころとなります。
 今回のようにメンテナンス自体もなるべく自動化することで、E2Eテストのメンテナンスコストという悩みを解決し、最終的により良いサービスをスピーディーにお届けできればというのが個人的な思いです。

CORECを支える技術 ~ログ監視編~ fluentd-AmazonSNS連携

こんにちは。なべです。
CORECの開発を担当しています。
CORECを支える技術 ~チケット駆動編~』に続き、『CORECを支える技術』シリーズの第2弾としてCORECのログ監視について書きたいと思います。

旧ログ監視の問題と対策

当初、CORECのログ監視はログ集約にくっつける形で始まりました。
ログ集約はログファイルを一定時間ごとにバックアップする仕組みで、各サーバーのログをfluentdで集め、1時間ごとに圧縮しS3に格納しています。
ログ監視は、その集約されたログを社内の監視システムで取得し、エラーログがあった場合にメールとIRCで通知するというものです。

この仕組には大きく以下の2つの問題がありました。

  • タイムラグ
    ログの監視をできるタイミングがS3に圧縮ファイルがアップされた後なので、検知するまで1時間のタイムラグが発生する可能性がある。
  • サービスレベル
    ログ監視を社内のサーバーに配置していて、消防点検や停電または、サーバーメンテナンス時に監視できないなどサービスレベルにも問題がある。

これらの問題に対応するためfluentdに集めたタイミングでエラーログを抽出し、AmazonSNS経由でメール通知することにしました。
これにより、社内のサーバーも経由しなくなり、タイムラグの問題・サービスレベルの問題を同時に解消できました。

CORECログ監視概要図

実施内容

この対応で実施したことは以下の4つです

  1. fluentdにプラグイン追加
  2. AmazonSNSに通知用Topic作成
  3. メールBody整形用のERB追加
  4. fluentdの設定ファイル記述

1. fluentdにプラグイン追加

以下の2つのプラグインを追加しました。

fluent-plugin-sns
Amazon SNS通知用
fluent-plugin-rewrite-tag-filter
エラーのフィルタ・レベルごとの分岐処理用
fluentdはrubyで動いているのでRubyistならお馴染みのgemコマンドでインストールできます。
この時fluentdが使っているrubyの環境を指定する必要があります。
特にfluentdにバンドルされたrubyを使用している場合は注意してください。
sudo /usr/lib/fluent/ruby/bin/gem install fluent-plugin-sns
sudo /usr/lib/fluent/ruby/bin/gem install fluent-plugin-rewrite-tag-filter

2. AmazonSNSに通知用Topic作成

Amazonの管理コンソールからSNSサービスを選択し、画面に従ってTopic及びSubscritionを作成します。 特に迷うことはないかと思いますが、注意するポイントは以下の2点です。

  • メールアドレスを登録するとメールがくるのでクリックして認証すること
  • 作成したTopicの権限設定を確認する
    権限設定とは作成したTopicに対して、通知に利用するAWSアカウントがこのトピックに投稿する権限のことです。もし権限がない場合には、[ポリシーの編集]から許可を与えてください。

3. メールBody整形用のERB追加

CORECの場合、fluentdが受け取るレコードは改行が\nにエスケープされていて読みづらいため、テンプレートで改行コードに変換しました。
以下のコードをerbファイルとして保存しておきます。
これだけのコードですが受け取った側の負担がだいぶ違います。特にスタックトレースで威力を発揮します。

<%record.each do |k,v|%>
<%="#{k}:#{v.to_s.gsub(/\\n/,"\n")}" %>
<%end%>

4. fluentdの設定ファイル記述

それでは最後にメインとなるfluentdの設定ファイルの編集をします。
変更/追加のポイントは以下の3点です。

4-a. copyプラグインで処理を2系統に分岐する
4-b. rewrite_tag_filterプラグインでエラーレベルごとにフィルタリングして、新しいタグを設定する
4-c. snsプラグインで通知するためのmatchディレクティブを追加する

それぞれ簡単に説明します。

4-a. copyプラグインで処理を2系統に分岐する

fluentdは最初にマッチしたmatchディレクティブだけを実行しますので、同じレコードに対して複数の処理をしたい場合にはcopyプラグインを使用します。
※copyプラグインは標準のプラグインなのでインストールをする必要はありません。

matchディレクティブと同等の内容をstoreタグに書くことができ、同じレコードに対して処理を複数記述できるようになります。
今回の対応では従来のS3への送信する処理をcopyで分岐し、SNSに送信する処理を追加します。

<match tag.log>
  type copy
  <store>
        従来の処理
  </store>
  <store>
        追加したい処理
  </store>
</match>

4-b. rewrite_tag_filterプラグインでエラーレベルごとにフィルタリングして、新しいタグを設定する

ただ単に前項の「追加したい処理」にSNS送信のディレクティブの内容を書いてしまうと、アプリケーションが出力したログすべて(エラーでないものも含めて)通知されてしまいます。
そこでログの内容によって処理を振り分けるにはrewrite_tag_filterプラグインを使用します。
レコードのキーを指定して、検索する文字列または正規表現を指定し、それにマッチした場合に新しいタグを指定する仕組みです。
今回はログレベルがFATALもしくはERRORの場合にシステム通知がしたかったので、ログレベルが格納されているキー(lebel)に対して、FATAL・ERRORそれぞれルールを作成しました。
複数のルールを設定するにはrewriteruleの後の数字をインクリメントして記述します。

  <store>
    type rewrite_tag_filter
    rewriterule1 level FATAL corec.rails.sns_fatal
    rewriterule2 level ERROR corec.rails.sns_error
  </store>

4-c. snsプラグインで通知するためのmatchディレクティブを追加する

typeにsnsを指定して、内容にはAWSユーザーにはお馴染みのパラメータ群を設定していきます。 sns_body_templateのパスはtd-agentが参照可能な場所にERBを配置して指定してください。

<match corec.rails.sns_fatal>
  type sns
  sns_topic_name for_system_notice_topic
  aws_key_id xxxxxxxxxx
  aws_sec_key xxxxxxxxxx
  sns_endpoint sns.ap-northeast-1.amazonaws.com
  sns_subject [COREC ALERT]corec/rails
  sns_body_template /etc/td-agent/erb/sns_body_template.erb
</match>

fluentd設定ファイルイメージ(全体)

変更前後の設定ファイルは以下のとおりです。
行はだいぶ増えていますが、それぞれの設定内容に難しいところはありませんでした。

  • 設定ファイル:修正前
<match corec.rails>
    S3に送信する設定(省略)
</match>
  • 設定ファイル:修正後
<match corec.rails>
  type copy
  <store>
        従来のS3に送信する設定(省略)
  </store>
  <store>
    type rewrite_tag_filter
    rewriterule1 level FATAL corec.rails.sns_fatal
    rewriterule2 level ERROR corec.rails.sns_error
  </store>
</match>
<match corec.rails.sns_fatal>
  type sns
  sns_topic_name for_system_notice_topic
  aws_key_id xxxxxxxxxx
  aws_sec_key xxxxxxxxxx
  sns_endpoint sns.ap-northeast-1.amazonaws.com
  sns_subject [COREC ALERT]corec/rails
  sns_body_template /etc/td-agent/erb/sns_body_template.erb
</match>
<match corec.rails.sns_error>
 (省略)
</match>

設定ファイル修正後、fluentdを再起動すれば反映されます。
※fluentdの連携をしている場合には送信元のfluentdも再起動することをおすすめします。
 私が設定した時には送信側が受信側を見失ってログの送信が止まってしまいました。

新たな問題

前項までの対応により、旧ログ監視の問題は解消し、 リアルタイムで、かつサービスレベルの高い仕組みができました。
が、新たな問題がいくつか発生しました。

  1. フィルタリングが無くなったのでERROR・FATALのレベルのログが全部飛んでくる
  2. 異常が発生するとガンガンメールがくる
  3. IRCに通知されない

新たな問題 1. ERROR・FATALのレベルのログが全部飛んでくる
→ 対策: 随時チケット化してエラーログの根本原因を潰す!

実はログを検索してメールを飛ばす処理の中でホワイトリストでフィルタリングして、通知するものを絞っていました。
その仕組みが無くなったのでERROR・FATALのレベルのログが全部飛んでくるようになってしまいました。 fluentdでフィルタリングするという選択肢もありましたが、対応の必要のないFATALやERRORを出すシステム側を修正すべきということで、チケット化してエラーの根本対応を実施。

新たな問題 2. 異常が発生するとガンガンメールがくる
→ 対策: 覚悟しておく!

従来のログ監視は1時間に1回ログを検索してメールを飛ばす処理だったため過去1時間に何件エラーが発生していても1通の通知にまとまっていました。
それがリアルタイムでエラー1件毎に通知されるため、異常事態にはメールが大量に送信されてしまいます。
実際に外部ベンダー側にトラブルがあり、復旧までに4時間かかった際に断続的に300通送信されてきました。orz
しかし、異常が発生しているのだから通知はあるべきでそれを止めるのは本末転倒ということで受け入れることにしました。

新たな問題 3. IRCに通知されない
→ 対策:社内で導入を進めている新監視システムへ

メールと同時にIRCに通知して、業務時間中はメールよりも気づきやすいように対策していましたが、IRCは社内サーバーで稼働しているため利用できませんでした。
現状は通知のために旧監視システムと並行稼動をしていますが、実際にはリアルタイムに飛んでくるメールに十分に気づけています。
今後は社内で進めているSlackのサービスを利用した障害対応フローに移行する予定です。
AmazonSNSに通知先を増やすだけで対応できるはずです。

まとめ

  • fluentdにプラグインを追加することで簡単にAmazonSNS経由の通知を実現できました。
    こういったプラグインが充実しているのもfluentdの魅力だと思います。(fluentdの導入については『fluentd(td-agent) の導入』を参照してください。)

  • ログ監視も即時性・対障害性の高いシステムを構築して確実で迅速な検知が必要です!
    ログ監視は最後の砦であり将来発生する障害の芽を発見するアンテナだと思っています。これからもCORECとともにCORECを支える運用システムもブラッシュアップしていきます。

  • 無駄なログは技術負債。根本から除去しましょう!
    無駄なログがたくさん通知されてくると、本来拾わなければいけないログを見落としてしまう可能性があります。そのために通知するログを絞ることは大切ですが、安易にフィルタリングをするのではなく、一つ一つ原因を突き止めて対応すべきところはプログラムで対応し、技術負債を残さないようにしたいですね。

[Rails]Model/テーブル設計で必ず覚えておきたいSTI

開発チームの下田です。

Ruby on Railsについて、基本的なこと(modelやcontrollerが何か知っている程度)を理解されている方を対象としています。

BtoBクラウド受注・発注システムCORECはRailsで開発しています。 開発初期のコードを見直してみると失敗したなと思うことがあります。 中でも特に開発効率に影響しているなと思うのが、RailsのActiveRecordのSTI(Single Table Inheritance、単一テーブル継承)を活用しなかったことです。

STIを簡単に言うと、モデルを永続化するときに、どのクラスなのかというメタ情報を含めてデータベースに保存します。 これだけではわかりづらいと思うので、失敗した例と改善例からご紹介したいと思います。

悪い設計

仮に下記の仕様のコードを書くとします。

要件1:
 CORECではWEBアプリから発注書を送信できます。送信方法は次の3つから選べます。

  • WEBアプリ内で送信する
  • メールで送信する
  • FAXで送信する

この時、次のようなコードにしていました。
(項目名、メソッド名は意味が通じるように和名にしています)

# 注文書model 
class Order < ActiveRecord::Base
  include FAX送信モジュール
  validates :送信方法, :presence => true
 
  # WEBで送信するなら送信先IDが必須 
  validates :送信先ユーザ_id, :presence => true, if: ->{ 送信方法 == 'WEB' }
 
  # メールならメールアドレスが必須 
  validates :メールアドレス, :presence => true, if: ->{ 送信方法 == 'メール' }
 
  # FAXならFAX番号が必須 
  validates :FAX番号, :presence => true, if: ->{ 送信方法 == 'FAX' }
 
  has_many :FAX送信結果
 
  def 送信する
    self.送信済 = true
    validate!
    case 送信方法
    when 'メール'
      注文Mailer.メール注文書送信.deliver
    when 'FAX'
      FAX送信する
    end
    save
  end
 
  private
 
  def FAX送信する
    FAX送信結果.create(送信結果: FAX送信モジュール.送信)
  end
end


何が問題か

当初は送信するときだから、送信手段ごとに処理をcase-whenで振り分けることに違和感はありませんでした。しかし、開発を進めていると、同じコードがあちこちに現れて読みづらいコードになり始めました。

要件2:

  • FAXは送信に失敗することがあるので、結果判定をする
  • WEBとメールは必ず成功したとみなす
class Order < ActiveRecord::Base
  # ~~ 略 ~~ 
 
  def 送信成功?
    # メソッドごとに送信方法による分岐がある状態 
    case 送信方法
    when 'FAX'
      FAX送信結果.送信成功?
    default
      true
    end
  end
end


あるべき姿

FAXもメールもすべて注文書の情報なのでOrderクラスにしたことは正しいのですが、送信方法によってふるまいが違います。役割が同じでふるまいが違うなら、サブクラスを作りポリモーフィズムを持たせるべきです。

sti


class Order < ActiveRecord::Base
  def 送信する
    self.送信済 = true
    save
  end
 
  def 送信成功?
    false
  end
end
 
# WEBで送信する注文書クラス
class Order::Web < Order
  validates :送信先ユーザ_id, :presence => true
 
  def 送信する
    validate!
    super
  end
end
 
# メールで送信する注文書クラス
class Order::Mail < Order
  validates :メールアドレス, :presence => true
 
  def 送信する
    validate!
    注文Mailer.メール注文書送信.deliver
    super
  end
end
 
# FAXで送信する注文書クラス 
class Order::Fax < Order
  validates :FAX番号, :presence => true
 
  has_many :FAX送信結果
 
  def 送信する
    validate!
    FAX送信する
    super
  end
 
  def 送信成功?
    FAX送信結果.送信成功?
  end
 
  private
 
  def FAX送信する
    FAX送信結果.create(送信結果: FAX送信モジュール.送信)
  end
end

メソッドの中から冗長なcase文が消えます。また、relationやvalidationのふるまいも送信方法ごとに整理され、見通しが良くなりました。

このようなクラス設計にしたい場合はSTIを使用します。STIを使用するにはtype:stringカラムを追加します。

  create_table "orders", force: true do |t|
    t.string   "type" # 追加するとSTIになる 
    t.integer  "送信先ユーザ_id"
    t.string   "メールアドレス"
    t.string   "FAX番号"
    t.datetime "created_at"
    t.datetime "updated_at"
  end

typeカラムがある場合とない場合を比較してみましょう。

# typeカラムがない場合 
order = Order::Mail.new(メールアドレス: 'hoge@example.com')
order.save
# id: 1 
# メールアドレス: "hoge@example.com" 
 
# OrderからロードするとOrderになる 
Order.find(order.id)
=> #<Order id: 1, メールアドレス: "hoge@example.com"> 
 
# Order::MailからロードすればOK 
Order::Mail.find(order.id)
=> #<Order::Mail id: 1, メールアドレス: "hoge@example.com"> 
# typeカラムがあり、STIになっている場合 
order = Order::Mail.new(メールアドレス: 'hoge@example.com')
order.save
# type: 'Order::Mail' # クラス名がtypeに自動的にsaveされる 
# id: 1 
# メールアドレス: "hoge@example.com" 
# created_at: 2015-01-01 12-34-56 
# updated_at: 2015-01-01 12-34-56 
 
# Orderからロードしても、きちんとOrder::Mailになる 
Order.find(order.id)
=> #<Order::Mail id: 1, メールアドレス: "hoge@example.com"> 
 
# サブクラスからロードすると、typeを検索条件に自動で加えてくれる 
Order::Mail.all.to_sql
=> "select * from orders where type = 'Order::Mail'"

データベースに保存し永続化した後でもふるまいを維持し続けてくれるので、クラス設計を適切に行えば複雑な仕様が追加されても、良い状態のコードを保てると思います。

運用上の注意

開発には非常に便利なSTIですが、運用を考えて開発しなければならないことが1点あります。typeカラムに存在しないclass名が設定されていると、ActiveRecord::SubclassNotFound例外が発生します。 ActiveRecord::SubclassNotFound例外が発生するのは、2パターンがあります。

  • リリース時に並行稼動している時に、新しいAPで保存した新しいサブクラスを古いAPでロードした
  • リリースしたがロールバックし、新しいAPで保存した新しいサブクラスを古いAPでロードした<

どちらの場合も、何も変更しないサブクラスを作り、先行してリリースしておくと、予期せぬ例外が起きません。フェイルソフトになります。

# 仮置きする。 
class Order::NewClass < Order
end

もしくはデフォルトスコープに現バージョンで存在しているclassのみを指定すると、ロードされないので例外が発生しません。サブクラスを追加した時に追加し忘れたり、where句が思わぬところで効いたりしないか確認が必要です。

class Order < ActiveRecord::Base
  # 条件に加えてしまう。
  default_scope ->{ where(type: [Order, Order::Corec, Order::Mail, Order::Fax]) }
  # ~~ 略 ~~ 
end

おわりに

STI以外にも、ああすればよかったと思うことは多くあります。その中でもSTIについて書いたのは、知っているか知らないかでModelの設計に差が出てしまうため、気づいた時には手軽にはリファクタリングできなくなっているからです。その反面、ある程度の規模のアプリケーションでなければ使い道が無くRuby on Rails チュートリアルに載っていなかったりと軽視されがちです。 新しくmodelを設計するときには頭の片隅に置いておくといいと思います。

記事検索