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

CircleCIでElasticsearchのログが吐かれていない問題を修正する(続き)

井原(@ihara2525)です。

(引き続きめちゃ小ネタです)

CircleCIでElasticsearchのログが吐かれていない問題を修正する」でタイトル通りCircleCIでElasticsearchのログが吐かれるようになったのですが、その対応だと/var/log/elasticsearchディレクトリの権限を変更してあげたりする必要がありました。

/var/log/elasticsearchにログを吐く設定がこのコミットで入っていて、同時にpath_logsというオプションで場所を指定できるようになっています。

/tmp以下には書き込めるだろうということで、以下のようにクラスタを立ち上げてあげると良さそうです。

Elasticsearch::Extensions::Test::Cluster.start(path_logs: '/tmp/log/elasticsearch') unless Elasticsearch::Extensions::Test::Cluster.running?

これでcircle.ymlから設定を削除できて簡単になりました。

- - >
-   if [ -d /var/log/elasticsearch ] ; then
-     sudo chmod 777 /var/log/elasticsearch
-   fi

これも漢方デスク@michi_omochiが見つけてくれました。感謝です!

CircleCIでElasticsearchのログが吐かれていない問題を修正する

井原(@ihara2525)です。

(めちゃ小ネタです)

CircleCI上でelasticsearch-extensionsを使ってテストを動かすのように引き続きテストを行っているのですが、elasticsearch-railsが0.1.8になった頃?から、テストは通るものの以下のようなエラーが出るようになっていました。

...
Starting 1 Elasticsearch nodes...log4j:ERROR setFile(null,true) call failed.
java.io.FileNotFoundException: /var/log/elasticsearch/elasticsearch-test-box500.log (Permission denied)
...
java.io.FileNotFoundException: /var/log/elasticsearch/elasticsearch-test-box500_index_indexing_slowlog.log (Permission denied)
...
java.io.FileNotFoundException: /var/log/elasticsearch/elasticsearch-test-box500_index_search_slowlog.log (Permission denied)
...

気持ちは悪いしログも書かれていない、ということで修正しようと試行錯誤してみました。

どうもインスタンスが立ち上がった時点で以下のディレクトリができている模様。

ubuntu@box844:~$ ls -lF /var/log
drwxr-xr-x 1 elasticsearch elasticsearch     190 Oct 16 05:52 elasticsearch/

多分これがelasticsearchグループ/ユーザ、755なので、テストを動かしているubuntuユーザが書き込めず上記のエラーになっているぽいです。

というわけで、circle.ymlに以下を追加、/var/log/elasticsearchディレクトリがある場合は777にしてあげます。

dependencies:
  pre:
    - >
      if [ -d /var/log/elasticsearch ] ; then
        sudo chmod 777 /var/log/elasticsearch
      fi

これで無事ログが吐かれるようになりました!

ubuntu@box844:~$ ls -lF /var/log/elasticsearch
-rw-r--r-- 1 elasticsearch elasticsearch     0 Oct 16 05:52 elasticsearch_index_indexing_slowlog.log
-rw-r--r-- 1 elasticsearch elasticsearch     0 Oct 16 05:52 elasticsearch_index_search_slowlog.log
-rw-r--r-- 1 elasticsearch elasticsearch   215 Oct 16 05:52 elasticsearch.log
-rw-rw-r-- 1 ubuntu        ubuntu            0 Oct 28 10:41 elasticsearch-test-box844_index_indexing_slowlog.log
-rw-rw-r-- 1 ubuntu        ubuntu            0 Oct 28 10:41 elasticsearch-test-box844_index_search_slowlog.log
-rw-rw-r-- 1 ubuntu        ubuntu        83443 Oct 28 10:41 elasticsearch-test-box844.log

といってもこのログを参照することがほぼないのですが(泣)、気持ち悪さは解消しました!

CircleCI上でnpmパッケージを定期アップデート

出口 (@dex1t) です。

現在開発中のサービスでは、Railsを使いつつ、React.jsを導入しています。
React.jsを使うと必然的にBrowserifyなどのnpmのエコシステムを、Railsプロジェクトに取り入れることになります。 そうすると、Railsプロジェクトであっても、npmで管理するライブラリが増えていきます。今回はそのアップデートを自動化してみました。

CircleCIで Gemfile.lock を定期更新する

本題に入る前に、bundle updateの自動化について触れます。
ググると、やり方はいろいろ出てくるのですが、

  1. 開発用のサーバからcronで定期的にCircleCIのAPIを叩く
  2. circle.ymlのdeploymentセクションで、スクリプトを実行する
  3. そのスクリプト内で、bundle updateの実行と差分をプルリクエストする

というのが、基本的な流れです。詳しくは、 CircleCIを使ってbundle updateを定期実行する - Qiita をごらんください。

CircleCIで package.json を定期更新する

npmのpackage.jsonを定期更新する場合も、上記の流れと同様です。 今回は、3を行うスクリプトをgulpで組んでみました。

npm-check-updatesでpackage.jsonを更新する

package.jsonの更新には、npm-check-updates を利用します。

$ npm install -g npm-check-updates
$ ncu --upgradeAll

これだけで、npmパッケージの更新確認とpackage.jsonの書き換えを行ってくれます。便利!!
今回はこれをgulpから呼び出します。

package.jsonを更新してプルリクエストする

gulpからgitとgithubが扱えれば何でもいいのですが、今回は主に以下を使いました。

run-sequenceは、各gulpタスクを直列に実行するために使います。 また、gulpプラグインの読み込みを簡単にするため、gulp-load-pluginsも使っています。

これら組み合わせると、ちょっと長いですがこんな感じで、package.jsonの更新~プルリクができます。

var gulp = require('gulp');
var gulpLoadPlugins = require('gulp-load-plugins');
var $ = gulpLoadPlugins();
var ncu = require('npm-check-updates');
var github = require('octonode');
var branch = 'npm-update';
var runSequence = require('run-sequence');

gulp.task('ncu', function(cb) {
  ncu.run({
    packageFile: 'package.json',
    jsonUpgraded: false,
    upgradeAll: true
  }).then(function(){
    cb();
  });
});

gulp.task('createBranch', function(cb) {
  $.git.checkout(branch, {args:'-b'}, function(){
    cb();
  });
});

gulp.task('addAndCommit', function() {
  return gulp.src('package.json')
             .pipe($.git.add())
             .pipe($.git.commit('Upgrade package.json'));
});

gulp.task('pushBranch', function(cb) {
  $.git.push('origin', branch, function(){
    cb();
  });
});

gulp.task('sendPullRequest', function(cb) {
  var token = process.env.GITHUB_ACCESS_TOKEN;
  github.client(token).repo('bitjourney/journey').pr({
    'title': 'Update npm packages',
    'body' : '@bitjourney/developers Please :eyes:',
    'head' : 'bitjourney:' + branch,
    'base': 'master'
  }, function() {
    cb();
  });
});

gulp.task('autoNpmUpdate', function(){
  runSequence(['ncu', 'createBranch'],
              'addAndCommit',
              'pushBranch',
              'sendPullRequest');
});

注意点としては、プルリクエストを送るユーザの、GithubのPersonal access tokenをCircleCIに登録しておく必要があります。 CircleCI側のEnvironment variablesにて、GITHUB_ACCESS_TOKENという名前で登録しておけばOKです。

circle.ymlのdeploymentセクションで実行する

あとは、circle.ymlのdeploymentセクションで、gulp autoNpmUpdateを実行するようにすればOKです。 参考までに、以下のようにしました。

deployment:
  maintain-libraries:
   branch: master
   commands:
      - >
        if [ -n "${NPM_UPDATE}" ] ; then
          git config --global user.email user@example.com
          git config --global user.name 'username'
          gulp autoNpmUpdate
        fi

git configもgulp内でやってしまってもいいのですが、今回は外出ししています。 実際にCircle CIをAPI経由で叩く方法については、こちらを参考にしてください!

おわり

要点だけかいつまみましたが、これで弊社のbotがプルリクをくれるようになりました 🎉

f:id:dex1t:20150915163102p:plain

Elasticsearchを使ったテストを書くときにsleep 1するのはやめましょう

井原(@ihara2525)です。

以前RSpecでElasticsearchを使ったテストを書くというエントリの中で、

before do
  Post.__elasticsearch__.create_index! force: true
  Post.__elasticsearch__.refresh_index!
  page
  Post.import
  # Wait test cluster to index the created objects
  sleep 1
end

みたいなコードを書いていたのですが、いろいろと間違っていました。 まずimportがインデックスをつくってくれるので、

before
  Post.import
  # Wait test cluster to index the created objects
  sleep 1
do

でOK。 そしてsleep 1しているのはここにもあるように、Elasticsearchがインデキシングを1秒待つので、それが終わるまで、ということでした。

ただ、これだとタイミングによってインデックスが更新されておらず、テストが通ったり通らなかったりという辛い現象が発生してしまいます。

困っていたら漢方デスクのエンジニア@michi_omochiが対応策を調べてくれて、

before do
  Post.import
  Post.__elasticsearch__.refresh_index!
end

と、import後に即インデックスの更新を実行してあげれば解決しますと。素晴らしい。 さらに

before do
  Post.import(refresh: true)
end

refreshオプションを渡せばimportのみで同様のことが可能に。(この辺りの実装)

というわけで、sleep 1するのはやめて、refresh_idnex!を呼び出すか、import(refresh: true)として、明示的にインデックスを即更新してあげましょう。

これでインデキシングのタイミングでたまにテストがこける問題、無事解決しました。ありがとう、@michi_omochi

GulpでSVGスプライトとアイコン一覧を一発生成

出口 (@dex1t) です。 ベクター素材をSVGスプライト化し、インラインSVGとして利用する仕組みを作ったので紹介します。

アイコンフォントかSVG

アイコンフォントとSVGの違いは、ググるとたくさん出てくると思うのですが、

  • 現在開発中のサービスはモダンブラウザのみ対象にしている
  • SVGのまま利用すれば、フォント化する手間がなく管理もしやすい
  • 多色にも対応できる

という点から、今回はSVGを利用することにしました。 SVGを利用する方法もいくつかありますが、今回はSVGをスプライト化し、Inline SVGとしてHTMLに埋め込む形を取ることにしました。

デザイナーにSVGを追加更新してもらいたい

いまはサービスをガンガン開発してる段階なので、素材の追加や変更も多く発生します。 ベクター素材は、デザイナーがイラレ上で作っているので、書き出しからSVGスプライト化まで一貫してお願いできると、作業効率が上がりそうです。

いま一緒に作業しているデザイナーさんは、黒い画面がある程度使える方なので、 規定のディレクトリにSVG素材を入れてもらい、コマンドを一発叩くとSVGスプライトが生成されるような環境を作ります。

GulpでSVGスプライトを生成する

スクランナーとして、この手のプラグインが充実しているGulpを利用します。 今回は以下のGulpプラグインを組み合わせました。

こんな感じのgulpタスクを書くと、一発で生成できます。 なお、プラグインを毎度requireするのは大変なので、gulp-load-pluginsも使っています。

var gulp = require('gulp'),
    gulpLoadPlugins = require('gulp-load-plugins'),
    pl = gulpLoadPlugins();

gulp.task('build', function() {
  return gulp.src('src/*.svg')
    .pipe(pl.svgmin())
    .pipe(pl.svgstore({ inlineSvg: true }))
    .pipe(pl.cheerio(function ($) {
      $('svg').attr({
        style:  'position: absolute; width: 0; height: 0;',
        width: 0,
        height: 0
      });
    }))
    .pipe(gulp.dest('app/assets/'));
});

後述しますが、生成したSVGスプライトをGem化して管理しているため、生成先をapp/assets/配下にしています。

ついでにアイコン一覧用の静的HTMLも生成する

SVGスプライトを生成するだけでなく、SVGアイコンの一覧 (例: Material Icons) を生成すると開発時に便利です。 前述した、gulp-cheerioと、gulp-templateを組み合わせて、これも自動生成します。

このようなテンプレートを用意した上で、

<html>
  <head>
    <title>Bit Journey Symbols</title>
  </head>
  <body>
    <%= inlineSvg %>
    <h1 class='title'>Bit Journey Symbols</h1>
    <ul class='symbols'>
      <% _.each(symbols, function(symbol) { %>
        <li class='symbol'>
          <svg><use xlink:href="#<%= symbol.id %>"></use></svg>
          <div class='symbol-id'><%= symbol.id %></div>
        </li>
      <% }); %>
    </ul>
  </body>
</html>

以下の様なgulpタスクで、テンプレートにSVGメタデータを流し込み、SVGスプライトとアイコン一覧ページを同時生成します。

var gulp = require('gulp'),
    gulpLoadPlugins = require('gulp-load-plugins'),
    pl = gulpLoadPlugins(),
    templateFile = 'template.html';

gulp.task('build', function() {
  return gulp.src('src/*.svg')
    .pipe(pl.svgmin())
    .pipe(pl.svgstore({ inlineSvg: true }))
    .pipe(pl.cheerio(function ($) {
      $('svg').attr({
        style:  'position: absolute; width: 0; height: 0;',
        width: 0,
        height: 0
      });

      var symbols = $('svg > symbol').map(function (){
        return {
          id: $(this).attr('id'),
        };
      }).get();

      gulp.src(templateFile)
        .pipe(pl.template({
          inlineSvg: $('svg'),
          symbols: symbols
        }))
        .pipe(gulp.dest('public'));
    }))
    .pipe(gulp.dest('app/assets/'));
});

実際に作った一覧ページはこんな感じです。今回は適当にCSSを書きましたが、symbols-for-sketchのような既存のテンプレートを流用してもよさそうです。

f:id:dex1t:20150804180108p:plain

便利ヘルパーと合わせてGem化する

今回はRailsアプリで利用するので、これらを社内Gem (Rails Engine) 化しました。 Gem化することで、アプリ側からはバージョン管理ができて便利です。 Inline SVGは、アイコンフォントに比べて若干使う際のマークアップが面倒なので、Rails向けの簡単なヘルパーも用意しています。

まとめ

このような仕組みを作ることで、以下のフローをデザイナーにお願いできるようになりました。

  1. イラレからSVGを書き出す
  2. gulpコマンドを実行
  3. SVGスプライトと一緒に生成されるアイコン一覧ページで表示を確認
  4. プルリクエストを送ってもらう

エンジニアは、Railsアプリ側でGemをアップデートするだけで、最新版の素材が利用できます。 素材周りは全てデザイナーにお任せできるので開発スピードがあがりそうです 🎉

Apartment::TenantNotFoundをrescueするElevatorを使う

井原(@ihara2525)です。

apartmentを使ってサブドメインでテナントを切り替える場合、存在しないテナントのサブドメインにアクセスするとApartment::TenantNotFoundが投げられるのですが、これはApplicationController等のレベルではrescueできません。

というわけで、Rackのミドルウェアを追加してそこで吸収するのですが、apartmentでいうところのElevatorを継承して、Apartment::Elevators::Subdomainを親とするミドルウェアをつくってみます。

lib/middlewares/rescued_tenant_elevator.rb

require 'apartment/elevators/subdomain'

class RescuedTenantElevator < ::Apartment::Elevators::Subdomain
  def call(env)
    super
  rescue Apartment::TenantNotFound
    request = Rack::Request.new(env)
    Rails.logger.error "Apartment Tenant not found: #{subdomain(request.host)}"
    [302, { 'Content-Type' => 'text/hml', 'Location' => Rails.application.routes.url_helpers.root_url }, self]
  end

  # needed to work
  def each
  end
end

こんな感じで、superを呼び出してまずApartment::Elevators::Subdomainに処理を行ってもらい、例外が発生した場合は上記の例だとトップページに飛ばす、みたいな対応をしています。

さらに、これのテストも書いたので、それも載せておきます。

spec/lib/middlewares/rescued_tenant_elevator_spec.rb

require 'rails_helper'

RSpec.describe RescuedTenantElevator do
  include Rack::Test::Methods

  let(:test_app) { -> (env) { [200, env, 'app'] } }
  let(:app) { described_class.new(test_app) }

  context 'when the tenant is public' do
    it 'returns 200' do
      get('http://example.org')
      expect(last_response.status).to eq 200
    end
  end

  context 'when the tenant exists' do
    it 'returns 200' do
      get("http://#{SUBDOMAIN}.example.org")
      expect(last_response.status).to eq 200
    end
  end

  context 'when the tenant does not exist' do
    it 'redirects to service top page' do
      get('http://foo.example.org')
      expect(last_response.status).to eq 302
      expect(last_response.header['Location']).to eq 'http://localhost:3000/'
    end
  end
end

テナントがない(サブドメインがない)場合や、テナントがある場合は200を返しますが、テナントがない場合は上記のミドルウェアの実装に従って302を返し、所定のページにリダイレクトされることをテストしています。

何かのご参考になれば幸いです!

CircleCI上のCapybara::Poltergeist::JavascriptErrorを回避する

井原(@ihara2525)です。

CircleCIでPoltergeistを使ってテストを回しているのですが、PhantomJSのバージョンが古いため(2015/7/27現在1.9.8)、以下のようなエラーが出てしまいました。

Capybara::Poltergeist::JavascriptError:
  One or more errors were raised in the Javascript code on the page. If you don't care about these errors, you can ignore them by setting js_errors: false in your Poltergeist configuration (see documentation for details).

  TypeError: 'undefined' is not a function (evaluating 'ReactElementValidator.createElement.bind(
        null,
        type
      )')

バージョン2系になれば必要なくなるのですが、それまでの間、Gemfileにbind-polyfillを追加することでエラーを回避することができました。

source 'https://rails-assets.org' do
  gem 'rails-assets-bind-polyfill', '~> 1.0'
end

簡単ですが、以上です!