Railsアプリ開発の豆知識 その1

こんにちは。エンジニアの内山です。

今回は社内で個人課題としてRailsサンプルアプリを開発することになりましたので、その様子を書きます。 現在も引き続き取り組んでいますが、その途中経過を書きたいと思います。

私はRuby on Rails初心者の状態(前職で多少使っていましたがほぼ独学)で今年の6月にCluexにジョインし、 弊社の運営するwebサービス「mamanoko」に関する簡単なタスク対応をしていたのですが、 Railsに関する基本的な知識やベストプラクティス、その他周辺技術(特にフロント技術が苦手です・・・)についてまだまだ実力不足を痛感しており 実装で壁にぶち当たることもありました。

そういうわけで、「スクラッチでアプリを開発し、開発のアンチパターン・ベストプラクティスを知る」ことを目的に サンプルアプリを開発することにしました。 Rails開発に関するtips、チュートリアルやブログなどはとても参考になるものが多数ありますが、 レビューで受けたアドバイスや調べたこと(=今回学んだこと)を元に豆知識としてまとめたいと思います。 特に開発初心者の方の役に立つと私としてはとても嬉しいです。

作るもの

  • ブログアプリ:ユーザを作成してログインすると、記事の閲覧・作成などができる。また、記事には複数のキーワード(タグ)を設定することができる。
  • Rails, Gem, js, jQuery, bootstrapを使い、ユーザ認証や画像アップロード機能を実装し、スタイルも整える。
  • テストも書く。 ※学習時間も含め、約1ヶ月で実装。毎週開発リーダーのレビューを受ける。

設計

基本を押さえて、以下の方針で設計しました。

DOA

DOA(Data Oriented Approach)とは、サービスを開発する際に、データの設計から行うアプローチです。

webサービスでは機能変更などが度々発生しますが、データの構造の変更は頻繁にあるわけではないので、 設計段階でデータの構造をしっかり決めておくことで効率よく開発ができるという考え方です。 複数のシステムを開発する際にDBを共有するときにも重要です。 今回は以下のようなデータ構造にしました。 f:id:cluex-developers:20170908153348p:plain

REST

RESTの基本アクションを利用した設計です。例えば記事機能(articles)はこのような形で構成します。 この説明が分かりやすいと感じました。(http://igarashikuniaki.net/rails_textbook/crud.html

action 処理
#index 全記事を表示する画面
#show 選択した記事を表示する画面
#new 記事を新規作成する画面
#create #newからpostされた記事を作成し、#indexにリダイレクトする。画面は無し。
#edit 選択した記事を修正する画面
#update #editからpostされた記事を更新し、#indexにリダイレクトする。画面は無し。
#destroy 選択された記事を削除し、#indexにリダイレクトする。画面は無し。

実装開始

ということで実装に入っていきます。まっさらな状態で一瞬迷ってしまいますが、 DOAの考え方に従って、変更の少ないModel→Controller→Viewの順で作っていきます。 機能としては大きく記事機能(articles)、記事へのタグ付け機能(keywords)、ユーザ認証(users)がありますが、スモールステップでまずはarticlesの機能から作っていくことにしました。

「スモールステップでつくる」といっても「articlesのmodel、controller、viewからつくる」という単位ではおそらくダメで、DOAの考え方から、先に必要なモデル/DBはすべて作っておくべきです。「変更の可能性の少ないDB周りから実装を固める」というのが肝です。あとからモデルを追加してリレーションを設定したりするのは混乱の元なので・・・ このため、articles、keywords、articles-keywords(中間テーブル)のモデルとDBを先に作ります。(ただし、usersについてはdeviseというGemを利用して後ほど作ります。) なお、この記事で全てのリソースを取り上げるのは時間がかかるので、今回は記事機能(articles)を中心に見ていきたいと思います。

Model

このように実装し、マイグレーションしました。なお、本記事では明示していませんが、文字数制限等を設けたいのでそれらの設定を行なっています。

Modelです。入力文字数によってバリデーションをかけ、各テーブル間のアソシエーションを設定しました。

[app/models/article.rb(記事)]

class Article < ApplicationRecord
  validates :title, presence: true, length: { maximum: 30 }
  validates :text,  presence: true, length: { in: 10..1000 }

  attachment :image

  has_many   :articles_keywords
  has_many   :keywords, through: :articles_keywords
end
[app/models/articles_keyword.rb(中間テーブル)]

class ArticlesKeyword < ApplicationRecord
  belongs_to :article
  belongs_to :keyword
end
[app/models/keyword.rb(キーワード)]

class Keyword < ApplicationRecord
  validates :keyword, presence: true, uniqueness: true, length: { maximum: 20 }

  has_many   :articles_keywords
  has_many   :articles, through: :articles_keywords
end

migration

migrationです。各DBカラムに対して、必要に応じてオプションを設定しました。

[db/migrate/yyyymmddhhmmss_create_article.rb(記事)]

class CreateArticles < ActiveRecord::Migration[5.0]
  def change
    create_table :articles do |t|
      t.string     :title, null: false
      t.text       :text,  null: false
      t.string     :image
      t.references :user, index: true, foreign_key: true

      t.timestamps null: false
    end
  end
end
[db/migrate/yyyymmddhhmmss_keyword_article.rb(中間テーブル)]

class CreateArticlesKeywords < ActiveRecord::Migration[5.0]
  def change
    create_table :articles_keywords do |t|
      t.references :article, index: true, foreign_key: true
      t.references :keyword, index: true, foreign_key: true

      t.timestamps null: false
    end
  end
end
[db/migrate/yyyymmddhhmmss_keyword.rb(キーワード)]

class CreateKeywords < ActiveRecord::Migration[5.0]
  def change
    create_table :keywords do |t|
      t.string :name, null: false

      t.timestamps null: false
    end
  end
end
Model/Migrationに関する学び
  • 【Gemについてあらかじめ調べておく】 ここまで、あたかも想定通りに進んでいるかのように書いたのですが(その方が分かりやすいという判断もありましたが・・・)、実はUserモデルをいったん作成したものの削除したりしました。ユーザー認証機構としてdeviseというGemを使用する予定だったため、機能を実装しながら平行してdeviseの知識をインプットしていた折、必要な機能(ファイル)のほとんどがdeviseを導入することで簡単に生成できることが分かったためです。Gemやプラグインを利用するのがあらかじめ分かっている場合は事前に調べて手戻りを防ぐべきだなと、反省です。今後の実装の教訓としたいと思います。

  • 【DBのカラムにオプションをつける】 データ操作に関するバリデーションはActiveRecordによって実現しますが、予期せぬDB不整合を防ぐために、マイグレーションファイルのDBカラムにも各種オプションを設定すべきです。null値を許容しない「null: false」以外にも、以下のようなオプションがあります。DBの整合性を維持するために設定しておきましょう。

option 内容
string 文字列
text 長い文字列
integer 整数
float 浮動小数
decimal 精度の高い小数
datetime 日時
timestamp タイムスタンプ
time 時間
date 日付
binary バイナリデータ
boolean Boolean

Controller

このような実装にしました。CRUDの考え方に基づいた標準的な作りです。

[app/controller/articles_controller.rb]

class ArticlesController < ApplicationController
  before_action :set_article,        except: [:index, :new, :create]

  # GET /articles
  def index
    @articles = Article.order(id: :desc)
  end

  # GET /articles/:id
  def show
  end

  # GET /articles/new
  def new
    @article = Article.new
  end

  # POST /articles
  def create
    @article = Article.new(article_params)

    if @article.save
      redirect_to articles_path
    else
      render :new
    end
  end

  # GET /articles/:id/edit
  def edit
  end

  # PATCH /articles/:id
  def update
    if @article.update(article_params)
      redirect_to articles_path
    else
      render :edit
    end
  end

  # DELETE /articles/:id
  def destroy
    @article.destroy
    redirect_to articles_path
  end

  private

  def article_params
    params.require(:article).permit(:title, :text, :image)
  end

  def set_article
    @article = Article.find(params[:id])
  end
end
Controllerに関する学び
  • 【generatrorで作成されるファイル】 ジェネレータを使うと様々なファイルが自動的に作成できて便利ですよね。でもどのようなファイルが作られるかご存知でしょうか?このようなファイルが作られます。
$rails generate controller articles
Running via Spring preloader in process 44
      create  app/controllers/articles_controller.rb
      invoke  erb
      create    app/views/articles
      invoke  test_unit
      create    test/controllers/articles_controller_test.rb
      invoke  helper
      create    app/helpers/articles_helper.rb
      invoke    test_unit
      invoke  assets
      invoke    coffee
      create      app/assets/javascripts/articles.coffee
      invoke    scss
      create      app/assets/stylesheets/articles.scss

せっかく作成されたファイルですが、例えば「helperはいらないなぁ」というようなことがあると思います。そういうときは、config/applicaiton.rbを以下のように編集することで、不要なファイルが作成されなくなります。

[config/application.rb]
module Sample
  class Application < Rails::Application
    config.generators do |g|
      g.helper false
    end
  end
end

すると、指定したとおりhelperが作られなくなります。

$rails generate controller articles
Running via Spring preloader in process 108
      create  app/controllers/articles_controller.rb
      invoke  erb
      create    app/views/articles
      invoke  test_unit
      create    test/controllers/articles_controller_test.rb
      invoke  assets
      invoke    coffee
      create      app/assets/javascripts/articles.coffee
      invoke    scss
      create      app/assets/stylesheets/articles.scss

めったに使わないならrails generate controller articles --no-helperでいいんですけどね。。。私の場合、実際はcontrollerが必要になったらファイルを新規追加で作ってしまったりすることもありますが、generatorよく使う方は試してみてください。

  • 【deleteとdestroyの違い】 destroyアクションの中では、指定した記事を削除するという処理を行います。 削除にはdeleteメソッドまたはdestroyメソッドが使用可能ですが、その違いは「ActiveRecordオブジェクトを介する削除にはdestroy、そうでない削除にはdeleteを使う」と説明されます。ActiveRecordオブジェクトを介して処理を行うとは、「コールバックを通りバリデーションが行われてから削除される」ということを意味します。

ActiveRecordを利用すると「あるオブジェクトを削除したら、関連するオブジェクトも同時に削除する」という処理をコールバックによって実現することができます。複数のオブジェクト間で依存関係がある場合、コールバックを通ることでその依存関係も一緒に削除できます。


VIew

indexだけ載せます。若干スタイル等入っていますが、まだまだシンプルな状態です。

[app/views/articles/index.html.erb]

<div class='container'>
  <h1>#index</h1>

  <div class='center-block'>
    <% @articles.each do |article| %>
      <div class='well well-lg'>
        <h3><%= article.title %></h3>
        <%= simple_format(article.text) %>

        <%= link_to 'show', article_path(article) %>
        <%= link_to 'edit', edit_article_path(article) %>
        <%= link_to 'destroy', article_path(article), method: :delete %>
      </div>
    <% end %>
  </div>
</div>
Viewに関する学び
  • 【view側でソートしない】 今回のアプリでは最新の記事が一番上に来るように表示したかったので、当初はviewで以下のように実装していました。
…
<% @articles.reverse_each do |article| %>
…

が、必要な処理はviewに渡す前に行うべきであるという指摘に納得し、以下の通り書き直しました。 前掲のコントローラ内で以下の通り処理し、viewでは余計な処理をしないように修正しました。

[app/controller/articles_controller.rb]

def index
  @articles = Article.includes(:user).order(id: :desc)
end
[app/view/articles/index.html.erb]

…
<% @articles.each do |article| %>
…

ソートに限らず、Rails内でロジックを書くべき場所は常に意識しようと思います。


routing

articlesのみ設定した状態なのでこんな感じです。

[config/routes.rb]

Rails.application.routes.draw do
  resources :articles
end

次回予告

サンプルアプリ開発は引き続き取り組んでおり、現在、キーワード設定機能やユーザログイン機能などを実装した段階です。 最低限のCSSで整え、現在はこんな感じです。見た目についてもbootstrapを利用してさらにリッチにしていく予定です!

f:id:cluex-developers:20170908152754p:plain
続編のブログでは、Gemの導入などに関する豆知識などをお伝えできればと思います。

それではまた次回!

We’re hiring!!

弊社ではWeb / ネイティブアプリエンジニアを募集しております。
ご興味がありましたらお気軽にご連絡下さいませ!
エンジニアの方、ぜひ情報交換しましょう!

www.wantedly.com

www.wantedly.com

iOS Tips集

こんにちは。エンジニアの志村です。
Cluexでは7月に「ままのて」という、妊娠中〜育児中まで医師監修のQ&Aやメッセージをお届けするアプリをローンチしました。

appsto.re

上記に伴いNativeアプリ専門のチームが出来ました。
現在、私含め3人でままのての改善やAndroid版の実装を行っております。
社内では2ヶ月に1回のLT大会や毎日終業前に行う豆知識の時間(軽いTips等を1人2〜3分程度で話す時間)を利用して、積極的に知識の共有や新しい知識を習得するようにしております。

今回はその中で出た知識をまとめたものを文章化しました。(このシリーズ定期的に出したいなぁ)

bounds

スクリーンからみて、Viewのサイズや位置などを保持するプロパティ。

class HogeViewController: UIViewController {
    @IBOutlet weak var tableView: UITableView!

    override func viewDidLoad() {
        super.viewDidLoad()

        // スクリーン上のtableViewのwidthを取得
        var width = self.tableView.bounds.width
        // スクリーン上のtableViewのheightを取得
        var height = self.tableView.bounds.height
    }
}

by 井戸田

IPアドレスの取得

1. getifaddrs(&ifaddr)
2. if (flags & (IFF_UP|IFF_RUNNING|IFF_LOOPBACK)) == (IFF_UP|IFF_RUNNING)
3. freeifaddrs(ifaddr)

ポイント

  1. ifaddr変数のポインタを渡して、ifaddrに取得した内容を格納する。
  2. ビットフラグで状態確認している。flagsがUP, RUNNINGの時&&LOOPBACKではない時を指す。
  3. getifaddrs関数の中でifaddrにメモリ確保しているので、メモリ解放する為にifaddrを渡す。

参照

get an iPhone's device name and get Ip address in swift 3

by 高島

addSubview, addSublayerをした時にきちんとremoveしよう

addSubview, addSublayerをUIViewに対して行った場合、追加されたUIViewやCALayerは残り続ける。
もし、UITableViewのcellForRowAt等で当該処理を行った場合、スクロールの度にUIViewやCALayerが追加され、メモリを圧迫するだけでは無く、表示そのものが崩れる場合があるので注意。
なので基本的にaddSubviewやaddSublayerを使用する場合は、事前にremoveFromSuperview, removeFromSuperlayerを行うべきである。

    func addCornerRadius(corners: UIRectCorner, radius: CGFloat) {
        if let oldMaskLayer = self.layer.value(forKey: "maskLayer") as? CAShapeLayer {
            oldMaskLayer.removeFromSuperlayer()
        }

        let maskPath = UIBezierPath(
            roundedRect: self.bounds,
            byRoundingCorners: corners,
            cornerRadii: CGSize(width: radius, height: radius)
        )
        let maskLayer = CAShapeLayer()
        self.layer.setValue(maskLayer, forKey: "maskLayer")

        maskLayer.frame = self.bounds
        maskLayer.path  = maskPath.cgPath
        self.layer.mask = maskLayer
    }

上記コードはUIViewに対して特定のcornerのみにradiusを付ける関数である。
上記コードで、もしif let oldMaskLayer〜が存在しない場合 && tableView.reloadData()を行い、heightが再計算される場合、前回使用していた高さでCAShapeLayerが描画されており、2重に線が描画される恐れがある。

by 志村

iOS9におけるremoveObserverの扱い

iOS9より、NotificationCenterはaddObserverで受け取ったオブザーバをweakで参照するようになる。
なので、明示的にremoveFromObserverをする必要がなくなった。

// Before iOS9
override func viewDidLoad() {
    super.viewDidLoad()

    NotificationCenter.default.addObserver(self, selector: "update:", name: MyNotification, object: nil)
}

deinit {
    NotificationCenter.default.removeObserver(self, selector: "update:", name: MyNotification, object: nil)
}

//  iOS9
override func viewDidLoad() {
    super.viewDidLoad()

    NotificationCenter.default.addObserver(self, selector: "update:", name: MyNotification, object: nil)
}

deinit {
}

参考

swift - NSNotificationCenterのremoveObserverについて - スタック・オーバーフロー

by 志村

以上になります。 このような感じで毎日知識を共有するようにしております。 次回からAndroid版も出来ると良いなぁ…

We’re hiring!!

弊社ではWeb / ネイティブアプリエンジニアを募集しております。
ご興味がありましたらお気軽にご連絡下さいませ!
エンジニアの方、ぜひ情報交換しましょう!

www.wantedly.com

www.wantedly.com

Docker with ECS × Railsを実現させるために考えたこと(CI編)

こんにちは。エンジニアの志村です。
前回・前々回とRailsをDocker運用する記事を書いておりますが、今回はCI編です。
過去の記事は下記になります。

cluex-developers.hateblo.jp

導入編でも書いた通り、CIはJenkinsを使用することにしました。 Dockerでの運用前はCircleCIを使用しておりました。

現在の構成

構成

MasterとSlaveの2段構成にしています。
Masterにジョブを置くとジョブの管理が面倒になるのと、ジョブの組み合わせ次第では不具合が起きる可能性があるので、Masterにはジョブを持たせず、テストやlint等はslaveで行うように設定しています。
Master, Slave共にDocker + ECSで構築しています。
ECSでSlave用のクラスタを用意し、

Amazon EC2 Container Service Plugin - Jenkins - Jenkins Wiki

を使用してSlaveを管理しています。

  • Pipelineを使用。Pipelineは全てJenkinsfileにて記述し、Gitリポジトリで管理
  • Docker+ECSでMaster & Slaveを管理
  • docker-composeでテスト・lintを走らせる

上記のように、Jenkinsコンテナ内でさらにRailsのDockerコンテナを起動している状態となります。所謂Docker in Dockerです。
調べたところ、Docker in Dockerには賛否両論あるようです。
ホストマシンのsocketをマウントし各コンテナで使用するように設定してあるので、今回の例だと、RailsのコンテナからJenkinsのコンテナが見えてしまうという問題があります。
しかし、

  • Dockerfileに記述すれば誰でもJenkinsの設定を変えることが出来る
  • ECSを使用して簡単にSlaveを管理出来る

という利点から今回はこのDocker in Dockerの方式を採用しました。

フロー

  1. Githubのpush or Jenkinsからの操作
  2. GithubのStatusをPendingに変更
  3. docker-compose build
  4. rubocop
  5. scss-lint
  6. slim-lint
  7. RSpec
  8. (Deployモードなら)docker build
  9. (Deployモードなら)ECRへのプッシュ
  10. (Deployモードなら)harmonik*1を使用したデプロイ

というフローになっております。
フローは4種類となり、それぞれJenkinsfileで管理するようにしています。

環境 モード
production deploy
production test
staging deploy
staging test

モードはtest・deployの2種類です。
testモードはRSpecまで(master, developブランチならば当該サーバに自動デプロイ)、deployモードはdocker build→デプロイまでを行うように設定しています。
こうすることによって、テスト無しでドッグフーディング環境にデプロイすることが出来、ビジネスサイドのチェックも素早く行うことが出来ます。

Jenkinsfile

Jenkinsfileはフェーズごとにファイルを分けました。

Jenkinsfile
Jenkinsfile.test
Jenkinsfile.build
Jenkinsfile.deploy

と言った具合です。
JenkinsfileはGroovyで記述します。

Jenkinsfile

環境やモードによってどの処理をするか、クレデンシャルの読み込み、またGithubのStatusを替えるといった処理を記述しています。

  1. Jenkinsのパラメータから環境・モードを判別
  2. Slackに開始を通知
  3. mode == ‘test’の場合、GithubのStatusをPendingに変更
  4. それぞれに必要なCredentialをS3からダウンロード
  5. deployモードならbuild, deployファイルの処理を行う
  6. testモードならtestファイルの処理を行う。master, developブランチならさらにbuild, deployファイルの処理を行う
  7. 終了通知をslackに送る

Jenkinsfile.test

testモードで使用するファイルです。主にテストやlintを回します。

  1. テストで使用するENVをS3からダウンロード
  2. docker-compose build
  3. bundle install
  4. Rubocop
  5. scss-lint
  6. slim-lint
  7. rake db:create && rake db:schema:load
  8. RSpec

Jenkinsfile.build

deployモード、またmaster・developブランチのtest終了後に使用します。 コンテナをビルドし、ECRにプッシュするところを担当しています。

  1. 各コンテナに使用するクレデンシャルをS3からダウンロード
  2. 各コンテナをビルド(タグはブランチ名+コミットハッシュ)
  3. 各コンテナをECRの各レポジトリにプッシュ

とこちらも至ってシンプルです。

Jenkinsfile.deploy

こちらは前回の記事で記述したデプロイツールのharmonik*2を使用します。

  1. バッチコンテナのデプロイ
  2. メンテナンスコンテナのデプロイ
  3. Rails関連コンテナのデプロイ

とこちらも比較的シンプルです。 Jenkinsfileを分割することにより、共通するコードをまとめることが出来、非常に見通しが良くなりました。

と弊社では上記のようにJenkinsを運用しています。 最初の設定は大変ですが、Dockerを利用することにより管理コストの削減や興味がある人間が気軽にJenkinsを設定出来るようになりました。

*1:前回の記事で紹介したデプロイツールです

*2:前回の記事で紹介したデプロイツールです

Yarnを導入しました!

f:id:cluex-developers:20170628130919p:plain

こんにちは、エンジニアの井戸田です。

弊社が運用しているmamanokoはRuby on Railsで実装しております。
以前まではnpmを使用していたのですが、先日Yarnに移行しました。
今回はYarnのメリットやnpmからYarnへの移行方法をDockerおよびDocker Composeを使用して書いていきます。

Yarnの特徴

厳密にバージョンを固定

Yarnの場合パッケージのインストール時にデフォルトで yarn.lock というファイルが生成されます。これによって複数人の開発時でも同じパッケージがインストールされるので、バージョンの違いで1人だけエラーが出るということが無くなります。

この機能はnpm v4までの shrinkwrap にあたります。ただ shrinkwrap の場合、package.json に手動でパッケージを追加後、それを削除すると追従することが出来ずエラーが発生し、結局 npm-shrinkwrap.json を削除して再インストールするなどの問題が発生していたようです。Yarnの場合そのようにnpmで悩んでいた依存性の問題も解決しております。

ただこの前リリースされたnpm v5ではバージョンを固定するようになったようです。

高速なインストー

npmの場合直列で1つ1つのパッケージをインストールしていたのに対して、Yarnの場合は並列でパッケージのインストールをしています。これにより高速にインストールすることができます。
また一度インストールするとキャッシュするので2回目以降のインストールはオフラインでも可能になります。

下記のURLを見てみると、スピードは Yarn > npm v5 > npm v4 の順に速いみたいです。npm v5でスピードが改善されたようですが、Yarnの方がまだまだ高速です。

github.com

よりセキュアに

パッケージのインストール前にチェックサムで整合性を確認しており、誤りがないかをチェックできるため信頼性が高まります。

本番運用するにあたって

弊社で運用しているmamanokoは、これまで本番環境にはAWSのEC2を使用していましたが、今年の3月にAWSのECS+EC2によるDockerでのサーバー運用にインフラを全面移行しました。

cluex-developers.hateblo.jp

開発環境もDocker及びDocker Composeで管理しており、本番環境との差異が以前に比べて格段に減りました。 またDockerfileとyamlで管理することにより、移行にあたって今までサーバーの中に入って作業しなければいけなかったのが、常にコードでインフラ環境を管理するようになりラクになりました。

導入方法

Docker Hubで配布されているnodeのコンテナを利用していきます。

https://hub.docker.com/_/node/

nodeコンテナのv8.1のコードを見てみるとYarnをインストールする記述があります。なのでDockerファイルにYarnをインストールするコードを書かなくても、すぐに使用することが出来ます。
ちなみにnpmをインストールする記述もあるのですぐに利用可能です。

# ~~~~~

ENV YARN_VERSION 0.24.6

RUN set -ex \
  && for key in \
    6A010C5166006599AA17F08146C2130DFD2497F5 \
  ; do \
    gpg --keyserver pgp.mit.edu --recv-keys "$key" || \
    gpg --keyserver keyserver.pgp.com --recv-keys "$key" || \
    gpg --keyserver ha.pool.sks-keyservers.net --recv-keys "$key" ; \
  done \
  && curl -fSLO --compressed "https://yarnpkg.com/downloads/$YARN_VERSION/yarn-v$YARN_VERSION.tar.gz" \
  && curl -fSLO --compressed "https://yarnpkg.com/downloads/$YARN_VERSION/yarn-v$YARN_VERSION.tar.gz.asc" \
  && gpg --batch --verify yarn-v$YARN_VERSION.tar.gz.asc yarn-v$YARN_VERSION.tar.gz \
  && mkdir -p /opt/yarn \
  && tar -xzf yarn-v$YARN_VERSION.tar.gz -C /opt/yarn --strip-components=1 \
  && ln -s /opt/yarn/bin/yarn /usr/local/bin/yarn \
  && ln -s /opt/yarn/bin/yarn /usr/local/bin/yarnpkg \
  && rm yarn-v$YARN_VERSION.tar.gz.asc yarn-v$YARN_VERSION.tar.gz

# ~~~~~

https://github.com/nodejs/docker-node/blob/31cfb8b96b69351b3552592dd1e2d62e73a5c5b9/8.1/Dockerfile

Dockerfileを作成して必要なコードを記述していきます。

docker/node/Dockerfile

# node v8.1のイメージを指定
FROM node:8.1

# コンテナに `/var/www/client/node_modules` ディレクトリを作成
RUN mkdir -p /var/www/client/node_modules

# `./client` を `/var/www/client` にコピー
# `./client/` はcss, javascriptなどasset部分の必要なファイルが入っているディレクトリ
COPY ./client/ /var/www/client/

# `/var/www/client` に移動
WORKDIR /var/www/client/

# `/var/www/client/package.json` を元に'Yarnを使用してインストール
RUN yarn install

# ローカルで配信するためのコード、本番にrelaseするためのコードなどを書いていく

npmとYarnでは、コマンドが少しずつ変わっているので注意が必要です。
下記のnpmとYarnのコマンドを比較した表を参考にしてみてください。

npm Yarn
npm install yarn install
(N/A) yarn install –flat
(N/A) yarn install –har
(N/A) yarn install –no-lockfile
(N/A) yarn install –pure-lockfile
npm install [package] (N/A)
npm install –save [package] yarn add [package]
npm install –save-dev [package] yarn add [package] [–dev/-D]
(N/A) yarn add [package] [–peer/-P]
npm install –save-optional [package] yarn add [package] [–optional/-O]
npm install –save-exact [package] yarn add [package] [–exact/-E]
(N/A) yarn add [package] [–tilde/-T]
npm install –global [package] yarn global add [package]
npm update –global yarn global upgrade
npm rebuild yarn install –force
npm uninstall [package] (N/A)
npm uninstall –save [package] yarn remove [package]
npm uninstall –save-dev [package] yarn remove [package]
npm uninstall –save-optional [package] yarn remove [package]
npm cache clean yarn cache clean
rm -rf node_modules && npm install yarn upgrade

まとめ

Yarnの特徴、移行についてをまとめてみました。
npmからYarnの移行に関して、npmのコマンドをYarnのコマンドに変更するだけで済んだのでとても手軽に出来ました。
またnpm v5からはYarnのlockファイルなどを取り入れており、Yarnもnpmもこれから発展していきそうです。

We’re hiring!!

Cluexではエンジニアサイド、ビジネスサイド共にメンバーを募集しています! お気軽にご連絡下さいませ!エンジニアの方、ぜひ情報交換しましょう!

www.wantedly.com

www.wantedly.com

5分でたどる!インフラアーキテクチャの歴史

f:id:cluex-developers:20170705174545j:plain designed by Creativeart - Freepik.com

こんにちは。エンジニアの内山です。 少しずつ暑くなってきましたね。季節の変わり目は体調を崩しやすいので、食事や運動により気を使っていきたいと思う今日この頃です。

今回はインフラのことについて書きます。私自身は普段Ruby on Railsを使って開発を行うwebエンジニアなのですが、以前インフラエンジニアとして仕事をしていた経験があります。日々プログラミングを行なっているとなかなかサーバやネットワークのアーキテクチャについては意識しないのですが、そういったインフラの変遷を知っておくこともweb開発の役に立つのではないかと考え、今回おさらいしてみようというわけです。

インフラとは何か

そもそもインフラとはなんでしょう。Wikipediaを調べてみましょう。 「インフラエンジニア」の項が参考になりそうです。

情報システムの構成要素には様々な技術が使用されているが、H/W、仮想化、OS、ネッ トワーク、ミドルウェア、セキュリティといった基盤となる領域を担当するのがインフラ エンジニアである。(中略)IaaS等のクラウドコンピューティングやデータセンター、システム運用も主要な職務範囲である。アプリケーションを含んだ全体最適化等に取り組むインフラエンジニアはITアーキテクトと呼ばれる。

引用元:インフラエンジニア - Wikipedia

つまりインフラとはITを使って仕事をしたり、サービスを受けたり提供したりするときに、その土台となる設備や情報機器のことですね。より具体的には、サーバやネットワーク機器を想像すると良さそうです。また、要件に応じたインフラの構成をアーキテクチャというようです。

集約型から分割型へ

システムの黎明期は、大型コンピュータ(ホスト、メインフレーム)で全ての業務を行う形態でした。いろんなアプリが一つのコンピュータに載っているイメージです。こういった構成を「集約型」と呼びます。 確かにコンピュータは1台ですから管理は簡単そうです。しかしあらゆる業務アプリが稼働しているわけですから、その1台が故障するだけで業務が止まってしまいます。故障しないように部品を多重化したりメンテナンスしたりするわけですが、コストがかさみます。 f:id:cluex-developers:20170705174950p:plain

集約型の特徴
 メリット
  - 構成がシンプルで管理しやすい。
 デメリット
  - 大型コンピュータの導入コストが高い。
  - 1台故障したときの影響範囲が大きい。
  - 停止しないように部品を多重化するなど、維持コストが高い。
  - 拡張性に限界がある。

そこで、各機能を複数の小型サーバに分けて一つのシステムを構築するという発想が生まれます。これを「分割型」と呼びます。安いコンピュータ(サーバ)を何台か調達して、それぞれのサーバで業務アプリを稼働させるのです。これなら1台壊れても全ての業務が止まってしまうことは避けられます。とはいえ、一台のサーバが故障すると、そのサーバで動いていたDBや他の機能まで使えなくなってしまうので、他システムとの連携などに影響が出てしまいます。 f:id:cluex-developers:20170705175006p:plain

分割型の特徴
 メリット
  - 小型サーバを使用して安価にシステムを構築できる。
  - 拡張性が高い。
 デメリット
  - 台数が増え、管理が複雑になる。
  - サーバが壊れたときの影響範囲はまだ大きい。

垂直分割型へ

上記の分割型アーキテクチャでは、サーバ故障時の影響範囲が問題になっていました。そこで、webシステムではおなじみの3階層型が登場します。すなわち、webサーバ、APサーバ、DBサーバの構造です。システムを分割し、サーバごとに単機能を持たせてやるという発想です。これならwebサーバが壊れたとしても他のシステムにDBのデータを連携でき、影響を抑えることができます。 f:id:cluex-developers:20170705175016p:plain

垂直分割型の特徴
 メリット
  - 特定のサーバへの負荷集中が改善される。
  - 特定のサーバで発生した障害が他のサーバに影響しづらい。
 デメリット
  - サーバの台数が増え、管理は依然として複雑である。

水平分割型へ

システムを3階層型で構成し、故障時の影響範囲を抑えることができました。垂直分割では機能ごとにサーバを分割しましたが、さらにユーザが増えてくると、今度は負荷を分散し信頼性を確保するために同じ機能のサーバを拡張していく必要が出てきます。 そこで最後に水平分割型を紹介します。同じ用途のサーバを増やすことで信頼性、性能を向上させます。これにより1台の故障が全体に与える影響がさらに下がります。また、こういったアーキテクチャでは多くの場合ロードバランサなどを利用して負荷を分散します。 f:id:cluex-developers:20170705175026p:plain

水平分割型の特徴
 メリット
  - 信頼性が向上する。
 デメリット
  - サーバの台数が増え、管理は以前として複雑である。

さらに進化はつづく・・・

インフラの変遷について駆け足で見てきましたがいかがでしょうか。インフラが進化していく過程は、さまざまなメリットを獲得したり課題を克服していく歴史だということがよくわかると思います。 上記の説明で、台数が増えて管理が煩雑になったサーバですが、後に仮想化技術やクラウド技術が登場することで大きく改善されていくことになります。

かつては多くの企業で物理サーバをオンプレミスで構築することが常識でしたが、クラウドの登場により、高い信頼性を確保しつつ、コストの面ではより安価に、調達リードタイムの面ではより迅速に(クリックするだけで!)構築できるのが今日のインフラです。 提供するサービスに応じて、最適なインフラを構築しましょう。 今後もいったいどのような革命的なインフラ技術が登場するのか楽しみですね。

(参考図書:山崎泰史他『絵で見てわかるITインフラのしくみ』、翔泳社

We’re hiring!!

Cluexではエンジニアサイド、ビジネスサイド共にメンバーを募集しています!
お気軽にご連絡下さいませ!エンジニアの方、ぜひ情報交換しましょう! www.wantedly.com www.wantedly.com

「Swiftデ、オ天気アプリヲ作成セヨ」

こんにちは。エンジニアの高島です。
先日、iOS開発にアサインされる前に大体の感覚を掴むために課題を頂きました。
その課題をこなす際に詰まったところを記事にさせて頂こうと思います。(課題未完)

課題の内容

事前講義

  • UIViewController, UITableView, UITableViewCellの説明
  • APIからJson取得してパースする時の流れの説明
  • Podfileの説明 ほか、基礎知識について

テーマ

「お天気アプリ」 ざっくりこんな感じです。 f:id:cluex-developers:20170627171210j:plain slackに送信するボタンをつける、Alertボタン押して都道府県画面に戻る機能は、一度基本機能を作った後で追加課題という事で実装しました。
その後、機能が一通り実装できたところで、クリーンアーキテクチャに則らせる(+RxSwift使う)という、3段階の流れで課題を進めています。

実装

以下、CleanArchitectureでの実装になります。
こういうやり方もあるよ!等ありましたら、是非ご意見頂けると嬉しいです。

詰まった箇所

1. mappingがうまくいかない

今回使用させて頂いたAPIは、livedoor天気情報
タイトル, 天気概況文, 今日の天気アイコンのURL, 今日の最高気温(摂氏), 今日の最低気温(摂氏)を使って実装しています。
敢えて今日のとつけたのには理由があって、これらの情報は今日、明日、明後日の3日分の配列から取得できます。
配列をどのようにObjectMapperでmapするのかは講義で学んだものの、ここでうまくパース出来なくて詰まりました。

解決策

構造体を別途準備して配列に持たせました。

struct ForecastEntity {
    var imageUrl: String?    // map["image.url"]
    var max: String?            // map["temperature.max.celsius"]
    var min: String?             // map["temperature.min.celsius"]
}

struct WeatherEntity {
    var title: String?                             // map["title"]
    var description: String?                 // map["description.text"]
    var forecasts: [ForecastEntity]?   // map["forecasts"]           ※ここに上の構造体を入れています
}

2. Entityが配列の時にいつ分けるのか

練習も兼ねて、都道府県名とIDをDataStoreに置く際に、タプルの配列型にしていました。
DataSotre

let prefectures: [(name: String, id: String)] = [
    ("埼玉", "110010"),
    ("東京", "130010"),
    ("千葉", "120010"),
    ("静岡", "220010"),
    ("茨城", "080010"),
    ("山梨", "190010"),
    ("群馬", "100010"),
    ("岩手", "030010"),
    ("福島", "070010"),
]

Entity

struct PrefectureListEntity {
    var list: [(name: String, id: String)]
}

といった形です。 しかし、Viewではセルに値を渡す為、配列でなく要素を取得する必要があります。
配列から要素に分けるタイミングで少し悩みました。

解決策

ViewModelの段階で要素を取得する様にしました。
ModelからViewで使える様にViewModelに変換しているのですが、その際に、

struct PrefectureListViewModel {
    func PrefectureViewModelProtocol(index: Int) -> PrefectureViewModelProtocol {
        return PrefectureViewModel(prefecture: prefectureList.list[index])
    }
    ...

この様な形で、配列から要素であるPrefectureViewModelを取得出来る様にしています。

3. xibを作らずにUIStoryboardの上にUITableViewCellを作る

都道府県リストだけでなく、天気画面やお問い合わせ画面もUITableViewを配置して、各セルに分けて作成しています。
例えば、お問い合わせ画面の場合、名前ラベルとテキストフィールドで1つ、といった形です。
クリーンアーキテクチャに則る前はそれぞれxibを作成していたのですが、則るタイミングで別途xibを作らずに、UIStoryboard上に全て配置して作成することにしました。 f:id:cluex-developers:20170627171214p:plain 変更してから実行してみたところ、dequeueResusableCellを呼ぶとCould not load NIB in bundleが出てセルが表示されなくなりました。

解決策

xibを使う際は必要なUINibの初期化部分を消しました。

let forecastNib = UINib(nibName: CellNames[Cells.Forecast.rawValue], bundle: nil)
tableView.register(forecastNib, forCellReuseIdentifier: CellNames[Cells.Forecast.rawValue])

4. UITextViewで高さ自動設定時に高さが0になる

天気画面とお問い合わせ画面はheightForRowAt, estimatedHeightForRowAtともにUITableViewAutomaticDimensionを返すようにしています。 この時、UITextViewの高さが0になる事に頭を悩ませていました。
これが実装中一番困ったことだったと言っても過言ではない程度には。

解決策

ConstraintsでHeightを付与しました。
Top, Bottomは与えていたので問題ないのかと思っていましたが、このHeightが重要でした。
なぜそうなのかは以下の記事で学ばせて頂きました。
http://cockscomb.hatenablog.com/entry/uitextview-on-uitableview

UITextView の intrinsic content size はどうかというと、そのままでは width も height も -1 の CGSize が返る。UITextView は UIScrollView を継承しているので、表示する内容に関わらず、ちょうどよいサイズというものを持たないのである。

現状

構成

f:id:cluex-developers:20170627171217p:plain

残り

  • キーボードに合わせてスクロールする量がキーボードの予測変換を考慮できてない(計算の仕方が単純におかしいのかもしれない)
  • Warningが出ている(Alertを二度表示しようとしていて出る, キーボード出した時に出る)

まだあと少し残っていますが、実装中に詰まったところはこのくらいでしょうか。
まだまだ写経してる感があるので、早く使いこなせる様になりたいです。

We’re hiring!!

Cluexではエンジニアサイド、ビジネスサイド共にメンバーを募集しています!
お気軽にご連絡下さいませ!エンジニアの方、ぜひ情報交換しましょう! www.wantedly.com www.wantedly.com

自己紹介 - 高島

f:id:cluex-developers:20170606100933j:plain

はじめまして!入社3ヶ月目のエンジニアの高島です。
今回は初ブログ記事ということで、技術記事は免除してもらい、自己紹介をさせて頂きたいと思います。

と言いましても、入社までの経緯や理由に関しては以前Wantedlyのインタビュー記事を書かせて頂きましたので、興味がおありでしたらぜひご覧ください。 www.wantedly.com

ここでは入社してからの所感について書かせて頂きますね。

入社してからの振り返り

振り返ってみて、この2ヶ月は今までの経験や考え方とのギャップを埋める期間だったなと感じます。

前提

自分の今までの開発経験は、専門学校で作成していたものを含めても(クオリティはさておき)、

機種 言語 内容
Windows C,C++ シューティング2D,3D
C++ ノベルゲーム
音ゲー要素持たせたブロック崩し
C# 帳票アプリ
Xbox C# アクション2D,3D
iPhone C++ ミニゲーム
アクション

など、思い出せる限りでこのような感じなので、今までWebに関わったことがありませんでした。 通信周りもなかなか理解ができなくて、避けて通って来ていましたし、ほぼWebの知識はないと言っても過言ではないところからスタートしました。

ギャップ1: Git

今までの開発環境で使っていたバージョン管理は、Subversionでした。 大した違いはないだろうと思っていたのですが、PullRequestやrebaseなど、耳慣れない言葉がポンポン出て来て戸惑いました。 SVNを使ってる時も、既にあるブランチにぽんぽんコミットしていったり、マージしたりしかしていなかったので、リポジトリに対しての意識もなかったですし、どうにもブランチ切るのがおっかなびっくりになります。 未だにPR出すのもドキドキします。

ギャップ2: インプットの習慣

これは本当にお恥ずかしい話なのですが、今までは自分の興味のある情報をたまーにチラ見したりする程度で、最新技術を追ったり、常に情報を仕入れたりはしてきませんでした。 最新ゲームや最近のゲームの動向は掴もうとしていましたが、それも海外のサイト見たりしてた訳でもないですし、今思えば甘かったなと思います。
Web界隈は範囲も広ければ、流行り廃りも早く、常に情報収集は当たり前の世界なので、生活の中でインプットしていく習慣をつけないとついていけないと思いますが、この習慣をつけることに今一番苦労しています。 技術系の基礎を身につけながら、情報のキャッチアップをしていくという2本立てにどう時間配分をするか、早く落としどころを見つけたいと思います。

ギャップ3: 公開までの工程

サイトの開発だと、Staging環境で動作確認をして、その後即リリースという流れですよね。 このリリースの早さが良くもあり、怖くもあります。 というのも、α→β→マスター版の工程や、テスターの存在になれきっていたのもあって、「ここで全部のバグを検知出来ないと本気でまずい!」という危機意識が薄いところがあったからです。 Cluexではコードレビューがあるのですが、それがなかったらと思うとぞっとしますね。もっと慎重さを持って、気をつけていかないといけないなと思います。 レビューは、まだまだ分からないところが多く、人がレビューしたのを見たり、質問するばかりですが、早く有意義なレビューをしていきたいと思います。

ギャップ4: ベンチャーでの開発

一人の采配が大きいという理由もあって、ベンチャーを選択しましたが、自分が予想している以上で、戸惑う程でした。 任せて頂いたタスクがユーザーに直結しているというのを日々痛感します。
また、苦手なところも自分でやらなくてはいけないのも、ベンチャーならではかなと思います。 今までは行き詰まって、ここで詰まっててこのくらい遅れそうですと報告すると、そのタスクは掬い取られて他のタスクを回される、ということが多かったので、苦手なものはあまり回されてきませんでした。 しかし、人数がそこまでいない状況だと自分が必要な分は自分で準備する必要があるので、苦手とも言ってられず、やるしかありません。 追い込まないと動かない(自分のような)人にはうってつけですね。ちょっと焦りますけど。

これから

大きくギャップがあったのはこの辺りでしょうか。
入社して2ヶ月と少しが経ち、今ようやく概要を掴めたかなと感じています。 これからは、使われている各技術についての基礎知識をつけていかなければと思います。 焦ると碌なことにならないのは今までの経験から分かっているので、焦らず急いで、しっかりアウトプットしながら一つずつ技術を習得していきます。

これからもよろしくお願いします。

We’re hiring!!

Cluexではエンジニアサイド、ビジネスサイド共にメンバーを募集しています!
お気軽にご連絡下さいませ!エンジニアの方、ぜひ情報交換しましょう! www.wantedly.com www.wantedly.com