Reversalを作る際に用いている雑多な技術についてメモっていきます。 今後も増えていくと思うので随時更新予定です。できるだけ楽しく開発できるようにモダンっぽい技術を採用するようにしてます。

インフラ

AWS

Infrastructure as Codeのためにできる限りAWSのリソースをコードで管理して運用してます。 愚直にRoute53 + EIP + EC2 + RDS + S3をコード化してIAMだけ手動管理してます。 ELBは今のところ使ってません。

Terraform

オーケストレーションはTerraform使ってます。情報の多さとTerraformingの存在が選択理由でした。

Packer + Ansible

プロビジョニングはPacker + AnsibleでAMIの作成を行ってます。アプリを導入する前のミドルウェアのインストールや環境変数の準備くらいまでさせてます。

GitHub + CircleCI + Capistrano

デプロイはGitHubのmasterの更新にフックしてCircleCIでTest + BuildとCapistranoを使ってデプロイまでさせてます。 CircleCI上でWebpackのBuildを実行してProduction環境でAssets Precompileをさせる構成です(微妙)

nginx + Unicorn

情報が多く、安定してそうだったのでWebサーバにはnginxをリバースプロキシにしたUnicornを採用しました。 WebSocketについては調査不足だったのでActionCableを使いたくなったら様子を見つつPumaに移行するつもりです。

Docker

Imperialの環境構築が割と面倒くさい上に状態を持つ必要が無いAPIなのでDocker化してEC2を1台割り当てて使ってます。 Reversal本体にDockerを使うのも検討しましたが現状ではコストをかけずダウンタイム無しのデプロイをするのにCapistranoの方が大分手っ取り早いと判断しました。

バックエンド

Rails 4.2

ReversalのバックエンドはRailsで色々ディレクトリ追加して使ってます。純粋なMVCに比べるとかなり粒度は細かくしてますができるだけRails Wayからはずれないようにはしてます。 JSはできるだけフロントでnode + Webpackに寄せてますが一部必要なのでjQuery+α程度をRails側で入れてます。

RailsはAPIに徹してフロントエンドをJSで全て書き、SPAにするのも考えました。軽くそのあたりを実装した結果、現時点でSSRを十分にしつつSPAを書くにはコストが高すぎると判断して普通にSlimでテンプレートを書くことにしました。 ページ読み込みの高速化についてはTurbolinksでできるだけ簡易に行っています。

DDDまで頑張ってるつもりはないですが単純なMVC構造はすぐ辛くなるので中規模Web開発のためのMVC分割とレイヤアーキテクチャ - Qiitaを結構参考にしてる感じです。 具体的には以下のディレクトリがあります。

  • assets
  • controllers
  • decorators
  • enums
  • helpers
  • infrastructures
  • models
  • notifiers
  • parameters
  • repositories
  • serializers
  • services
  • views

Rubyの採用理由について

Scala + Playなど他の言語の利用も検討ましたがRubyの書き心地のよさと情報の多さ、既存Gemの資産などのメリットを考慮してRuby + Railsの採用に至りました。 ディレクトリ構成を変えている部分についてはSinatraなどでもよかった気はしますが、Routingなどを含め全て一から設計するよりはRails Wayに乗った上で必要な部分を変更するのが最も良いと判断しました。

テストについてはリソースが追いつかずに全然書けてません、すいませんという感じです。

以下 Gemfile です。

source 'https://rubygems.org'
ruby '2.3.1'

gem 'rails', '~> 5.0.0'

gem 'active_link_to'
gem 'active_model_serializers'
gem 'activemodel-serializers-xml'
gem 'aws-sdk'
gem 'bootstrap_form'
gem 'ckeditor'
gem 'classy_enum'
gem 'counter_culture'
gem 'draper', '3.0.0.pre1'
gem 'fog-aws'
gem 'font-awesome-rails'
gem 'foreman'
gem 'gemoji'
gem 'google-analytics-rails'
gem 'html-pipeline'
gem 'html-pipeline-youtube'
gem 'http_accept_language'
gem 'jquery-datetimepicker-rails'
gem 'jquery-rails'
gem 'kaminari'
gem 'mysql2'
gem 'nico_search_snapshot'
gem 'omniauth'
gem 'omniauth-slack'
gem 'omniauth-twitter'
gem 'paperclip', git: 'https://github.com/thoughtbot/paperclip.git'
gem 'pry-rails'
gem 'public_activity'
gem 'rails_autolink'
gem 'ransack'
gem 'react-rails'
gem 'redis'
gem 'redis-rails'
gem 'ridgepole', git: 'https://github.com/winebarrel/ridgepole.git', branch: 'v0.6.5'
gem 'sass-rails'
gem 'seed_dump'
gem 'sitemap_generator'
gem 'slack-api', require: 'slack'
gem 'slack_markdown'
gem 'slim-rails'
gem 'therubyracer'
gem 'twitter'
gem 'uglifier'
gem 'whenever', require: false
gem 'yt'

group :development, :test do
  gem 'annotate'
  gem 'awesome_print'
  gem 'better_errors'
  gem 'binding_of_caller'
  gem 'brakeman', require: false
  gem 'bullet'
  gem 'dotenv-rails'
  gem 'guard-rspec', require: false
  gem 'hirb'
  gem 'hirb-unicode-steakknife', require: 'hirb-unicode'
  gem 'pry-byebug'
  gem 'pry-coolline'
  gem 'pry-doc'
  gem 'pry-stack_explorer'
  gem 'puma'
  gem 'rack-mini-profiler', require: false
  gem 'rails-erd'
  gem 'rails_best_practices', require: false
  gem 'rubocop', require: false, git: 'https://github.com/bbatsov/rubocop'
  gem 'rubycritic', require: false
  gem 'slim_lint', require: false
  gem 'spring'
  gem 'spring-commands-rspec'
  gem 'squasher'
  gem 'terminal-notifier'
  gem 'terminal-notifier-guard'
  gem 'view_source_map'
end

group :test do
  gem 'factory_girl_rails'
  gem 'rspec'
  gem 'rspec-rails'
  gem 'simplecov'
  gem 'webmock'
end

group :production do
  gem 'newrelic_rpm'
  gem 'rollbar'
  gem 'unicorn'
  gem 'unicorn-worker-killer'
end

group :deployment do
  gem 'capistrano'
  gem 'capistrano-bundler'
  gem 'capistrano-rails'
  gem 'capistrano-rbenv'
  gem 'capistrano3-unicorn'
end

Anaconda3(Python3.5 + OpenCV + numpy)

対戦組み合わせ推測エンジン、ImperialのバックエンドにPython + OpenCVを使ってます。 Pythonを触ったのはこれが初めてでしたがOpenCVの情報の多さとnumpyの速さが必要になりそうということで選択しました。

インストールは結構面倒くさかったのでDockerでAnaconda3を使ってUbuntuにバイナリを直接入れてます。

フロントエンド

Webpack

基本的にWebpackでビルドしたものをRailsのassets以下に入れてコンパイルするようにしてます。 最初はWebpackでmanifest.jsonを出力してSprocketsを完全に外してましたがreact-railsでSSRすることを検討した際に同ロジックで動作するStaging環境がないという理由でRails側に寄せました。 多少ビルド時間に差ができたりassets以下に生成したファイルが変に残っていて想定外の挙動を示すなどといったことがありますがそんなには困っていません。

が、Reactの採用はしばらくなさそうなのと、Hot Reloadなどが便利なので余裕ができたらwebpack-dev-serverを使うようにしたいです。

以下 webpack.config.babel.js になります。

import path from 'path';
import webpack from 'webpack';
import ExtractTextPlugin from 'extract-text-webpack-plugin';

const production = process.env.TARGET === 'production';
const devtool = production ? '' : 'inline-source-map';

const defaultPlugins = [
  new webpack.ProvidePlugin({
    $: 'jquery',
    jQuery: 'jquery',
    jquery: 'jquery',
    Tether: 'tether',
    'window.Tether': 'tether',
  }),
  new ExtractTextPlugin(path.join('stylesheets', 'webpack', '[name].css')),
];

const productionPlugins = [
  new webpack.NoErrorsPlugin(),
  new webpack.optimize.UglifyJsPlugin({
    compressor: { warnings: false },
    sourceMap: false,
  }),
  new webpack.DefinePlugin({
    'process.env': { NODE_ENV: JSON.stringify('production') },
  }),
  new webpack.optimize.DedupePlugin(),
  new webpack.optimize.OccurenceOrderPlugin(),
];

const plugins = production ? defaultPlugins.concat(productionPlugins) : defaultPlugins;

const output = production ? {
  path: path.join(__dirname, 'app', 'assets'),
  filename: path.join('javascripts', 'webpack', '[name].js'),
} : {
  filename: path.join('[name].js'),
  publicPath: 'http://localhost:3500/',
};

/* eslint-disable max-len */
const cssLoader = production ? ExtractTextPlugin.extract('style', 'css!postcss') : 'style!css?sourceMap!postcss';
const sassLoader = production ? ExtractTextPlugin.extract('style', 'css!postcss!sass') : 'style!css?sourceMap!postcss!sass?sourceMap';
/* eslint-enable max-len */

export default {
  entry: {
    bundle: './frontend/js/index.js',
    style: './frontend/css/index.js',
    vendor: ['jquery-ujs', 'sweetalert', 'turbolinks', 'tether', 'vue', 'whatwg-fetch'],
  },
  output,

  module: {
    preloaders: [
      { test: /\.css/, loader: 'stylelint' },
    ],

    loaders: [
      { test: /\.html$/, loader: 'html' },
      { test: /\.jsx?$/, loader: 'babel', exclude: /node_modules/ },
      { test: /\.css$/, loader: cssLoader },
      { test: /\.(sass|scss)$/, loader: sassLoader },
      { test: /\.jpg$/, loader: 'url' },
      { test: /\.png$/, loader: 'url' },
      { test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: 'url' },
      { test: /\.(otf|ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: 'url' },
      { test: require.resolve('turbolinks'), loader: 'imports?this=>window' },
    ],
  },

  resolve: {
    extensions: ['', '.js', 'jsx', '.css', '.sass', '.scss'],
    root: [
      path.resolve('./frontend/js'),
      path.resolve('./frontend/css'),
    ],
  },

  sassLoader: {
    indentedSyntax: 'sass',
  },

  /* eslint-disable global-require, max-len */
  postcss: [
    require('autoprefixer')({ browsers: 'last 2 versions' }),
    require('postcss-import')(),
    require('postcss-simple-vars')(),
    require('postcss-nested')(),
    require('postcss-assets')({ loadPaths: ['frontend/css', 'frontend/js', 'frontend/img', 'frontend/fonts'] }),
    require('postcss-mixins')(),
    require('postcss-extend')(),
    require('postcss-calc')(),
    require('postcss-short')(),
    require('csswring')(),
  ],
  /* eslint-enable global-require, max-len */

  plugins,
  devtool,
  devServer: {
    headers: {
      'Access-Control-Allow-Origin': 'http://localhost:3000',
      'Access-Control-Allow-Credentials': 'true',
    },
  },
};

Sass(Bootstrap v4.0.0-alpha.2 + Bourbon) + PostCSS

Reactを切った段階でReact CSS Modulesの恩恵を受けれないため現時点でのPostCSSのメイン採用は見送りました。 情報が多く、使い慣れたBootstrapとBourbonでSass形式でCSSを書いてます。

PostCSSはautoprefixerやminify、あと一部の便利なPostCSS Pluginのために利用しています。 Railsのassetsを利用するために画像・フォントのインライン化と絶対パスでの記述が行いたかったためにpostcss-assetsはSASSを書く上でも重宝してます。

Vue.js 1.0 + Jade

サイトの一部のインタラクティブなコンテンツはVue.jsとJadeで一気に書き上げました。

雑に書くことはできましたが、現状のJadeファイルをpugとして読み込むとエラーを吐いてしまうような状態な上、Vueの機能を十分に使っているとも言えない状態なので将来的にはRiotやReactで綺麗に書き直したいと思っています。

CKEditor

WISYWIGにはCKEditorを利用しています。スキンやプラグインが多く、Railsとの連携も公式にサポートされているため導入も比較的簡単でした。

jQuery

どうしても依存しているところが多々あるため今後の改修で積極的に削っていきたいです。

その他SaaS

Google Apps、CodeClimate、NewRelicなどを使っています。 死活監視の未導入など問題があるためStatusCakeあたりは早く導入したいと思っています。