アトラシエの開発ブログ

株式会社アトラシエのブログです

gem使わなくても簡単にできるrailsのtips

それgem使わなくてもできるよまとめ

gemは便利なのですが闇雲に入れるべきものでもありません。とくに用途が限られていて、わざわざgemを使うまでもないことや使用目的以上にgemが巨大な場合は簡単に自分で代替物を作ったほうがメンテナンスやコントロールが容易な場合もあります。

Devise

Deviseのような認証機構はシンプルなものであればこういったsessionにuser_idを格納するだけで十分です。

user.rb

class User < AR::Base
  has_secure_password
end

session_controller.rb

if user.authenticate(params[:password])
  sign_in(user)
end

application_controller.rb

def sign_in(user)
  session[:user_id] = user.id
end

def sign_out
  session[:user_id] = nil
end

def current_user
  User.find_by(id: session[:user_id])
end

def user_sign_in?
  !!current_user
end

Railsのsessionはデフォルトではcookieに暗号化された値として保存されます。基本的にuser_idのような固有値を入れてもクライアント側では復号化できないはずです。(secrets.ymlの値があれば復号できると思います)

セキュリティ上の優先順位としては SSL通信にする、cookieにsecure属性を付与する ことが重要で、理想的にはcookie storeではなくmemcacheなどをサーバ側に用意することでより安全になります ^1

仮にDeviseの主要機能であるメール認証、パスワードリセット、ロックなどをすべて実装するにしても自分で作ってもさほど難しくない上保守性が高いのでオススメです。(というよりDeviseを使っても結局Deviseのコントローラをオーバーライドする羽目になる気がします)

friendly_id

user.rb

class User < AR::Base
  before_create :set_uuid

  private

  def set_uuid
    self.uuid = SecureRandom.hex(10)
  end
end

routes.rb

get 'users/:uuid' => 'users#show', as: :user

users_controller.rb

def show
  @user = User.find_by(uuid: params[:uuid])
end

view.html.slim

= link_to user.name, user_path(user.uuid)

Railsのデフォルトである連番idが嫌な場合にfriendly idを使うケースがあると思いますが、単にルーティングをいじればいいのでこっちのほうが簡単です。 とりあえず適当な値が欲しいならSecureRandomを使えばまず大丈夫です。

secure randomで作った値を使うほうが権限チェックのミスがあっても漏洩リスクを多少緩和できると思います。

連番idのとき

/secure_photos/3

# 本当はcurrent_userから引っ張らないとダメ
# current_user.secure_photos.find(params[:id])
# 
SecurePhoto.find(params[:id])

/secure_photos/7997a08d65b56628

SecurePhoto.find_by!(uuid: params[:uuid])

といっても本気でアタックする人にとっては意味がないのでしっかり権限チェックしてください。

Draper

Draperのすべての仕組みを理解していませんが、SimpleDelegatorを使うだけで十分な気がしています。

user_decorator.rb

class UserDecorator < SimpleDelegator
  def h1_title
    "#{name}のページ"
  end
end

user.rb

include Decoratable

app/models/concerns/decoratable.rb

module Decoratable
  def decorate
    klass = begin
              "#{self.class.to_s}Decorator".constantize
            rescue NameError
              "#{self.class.base_class.to_s}Decorator".constantize
            end
    klass.new(self)
  end
end

これだけで使う時は@user.decorateとするだけでデコレータのクラスに変換されます。 この方法がいいのは自分でデコレータのクラスと生のモデルクラスをコントロールできる点、軽い点、適宜デコレータを増やしてdecorateメソッドを変えればモデルとデコレータを1対多対応させられる点にあります。

各viewやAPIごとにdecoratorを分割し、デコレータが必要な文脈ごとに変換をかけるのは少し手間ですが、暗黙に変換されると挙動がよくわからなくなることがあります。

画像アップロード

これは上記とは仕組みが違いますが、carrierwaveやminimagickを使わなくても画像をアップロードする仕組みは作れます。

簡単に言えば画像自体を受け取らず、アップロード先を別に持ってURLや識別子だけ受け取って保存すればいいのです。

Cloudinary

cloudinary.com

このようなサービスを使えば画像のストレージとサイズ変換などの面倒をすべて見てくれるので、gemでいえばcarrierwave, minimagick, fogあたりを省略できます。

まとめ

以上、少しやってみればgemを使わなくても労を少なく、下手をするとgemを使うより簡単に必要なことができることがあります。 gemを入れ過ぎるとgem同士がバッティングしたりバージョンアップに苦労します。もちろんgemのようにメンテナンスが繰り返されるソフトウェアには一定のメリットがあるので必要な局面では使うべきでしょう。例を上げればStateMachineは一見自分で実装可能なようで、ライブラリに依存したほうが確実に見通しがいいケースが多かったです(あくまで私の経験です)。

carrierwaveで画像フォーマットを複数持つ方法

carrierwaveはサンプルを読む限りでは保存形式をjpegとかpngとかに決め打って、versionではリサイズしかしないのが一般的な使い方のようです。

しかしこれだと「ネイティブアプリではwebp形式で配信し、ブラウザにはjpeg形式で配信したい」というようなユースケースに対応できません。

carrierwaveはこういったフォーマットのマルチ化ができないのかと思っていたのですが、コードレベルでちょっと工夫すれば簡単にできました。

class PictureUploader < CarrierWave::Uploader::Base

  version :limit_500x_jpeg do
    process convert: 'jpeg'
    process resize_to_limit: [500, 10000]
  end

  version :limit_500x do
    process convert: 'webp'
    process resize_to_limit: [500, 10000]
    def full_filename(for_file)
      super(for_file).sub(/\.jpeg/, '.webp')
    end
  end

  version :fill_500x500 do
    process convert: 'webp'
    process resize_to_fill: [500, 500]
    def full_filename(for_file)   # ・・・※2
      super(for_file).sub(/\.jpeg/, '.webp')
    end
  end

  def filename    #・・・※1
    "#{secure_token}.jpeg" if original_filename.present?
  end

  def extension_white_list
    %w(jpg jpeg gif png webp)
  end

  def secure_token
    if model.attributes[mounted_as.to_s].present? && !model.send("#{mounted_as}_changed?")
      model.attributes[mounted_as.to_s].split('.').first
    else
      get_secure_token
    end
  end

  def get_secure_token
    var = :"@#{mounted_as}_secure_token"
    model.instance_variable_get(var) or model.instance_variable_set(var,  SecureRandom.uuid)
  end
end

このように、まずは全体のfilenameをjpegで決め打って(※1の部分)、あとは各バージョンでプライベートメソッドをオーバーライドして(※2の部分)filenameを変更します。 これだけでアップロード先のURLも画像URLもうまく対応できます。

ちなみに本当は※1の部分でjpegを決め打たずにextensionなしのfilenameを格納し、各バージョンごとにextensionをつけたほうが見た目は綺麗なのですがその方法だとmodel.picture.urlというバージョン抜きのURLの拡張子に対応できないので、少し違和感がありますがsubで置換させることにしました。

ちなみにsub置換のコストは手元のベンチマークでは誤差レベルです。

carrierwaveの限界

とはいえ、個人的にはcarrierwaveは便利ですが限界があるように感じてきました。まずリサイズに時間がかかること、画像のサイズ変換というあまりアプリケーションのビジネスロジックに関係ない計算コストを持たないといけないこと、サイズの決め打ちがマルチデバイス対応で辛いこと等です。

大手だと独自のリサイズ配信システムをnginxモジュールなどで作ってCDNを咬ませてキャッシュさせるようなことをしていると思いますが、これも知識を持った人でないと開発・運用は難しいでしょう。

cloudinary.com

あまりはやってないですがこういう配信サービスもあるので、これを利用するのも手です。 また、google app engineでは標準で画像配信を動的に行う仕組みを持っているので、配信主体のアプリケーションはappengineで作るか、配信部分だけでもapp engineで社内サービス化するのがいいかも。

qiita.com

sidekiqを使用する際に注意したい覚え書き

Railsで非同期処理を行う際にデファクトになりつつあるSidekiqですが、実際の運用ノウハウや少し踏み込んだトラブルシューティングは意外とまだウェブ上にリソースが不足しているという印象があります。そこでいくつか、基本的なことから少し踏み込んだ話まで、いくつか紹介したいと思います。

Sidekiqの導入・運用

Sidekiqの導入にはRedisが必要であるということはよく説明されるのですが、もう少し正確に言うとRedisをデータ保存先としてSidekiqというプロセスがスレッドベースで動きます。したがって正常にSidekiqが動くためには通常のRailsのプロセス(unicornやpassenger)のほかに、redis-serverのプロセス、Sidekiqのプロセスが動き続けていることが必要になります。(厳密に言えばSidekiqだけ動けばいいならRailsプロセスは不要です)

redis-serverが死んでいるとSidekiqはエラーを起こしますが、Sidekiqが死んでいてもredisに待機状態のジョブが積まれるので、あとからSidekiqプロセスを立ち上げれば積んでいるJobを消化していきます。 Sidekiqのプロセスは/app/jobs/app/workersのコードを変更したら再起動する必要があるなど、頻繁に停止・起動を繰り返します。 redisの死活監視やデータ容量などを気にしたくない場合はRedisToGoという選択肢もあります。

redistogo.com

ただ、RedisToGoはわりと高価なのと、Sidekiqに必要なデータサイズは大したことがないので小さいサービスであればアプリケーションサーバの一つをredisサーバ併用にしてもいいかと思います。

デプロイ

qiita.com

この記事で解説されていますが、Sidekiqの更新反映はけっこうたいへんです。また、Sidekiqがなんらかの事情で死んでしまった時に手元から起動させるということもあるので、Capistranoの導入はほぼマストな気がします。 この記事にあるように、capistranoでリリースしたとしてもSidekiqのワーカーが完了してから停止するわけではないので、ワーカーが途中で中断した際にもう一度実行しても同じ結果が得られるように(処理の冪等性が保証されるように)ジョブをデザインしなければいけないでしょう。

具体的には、メールを送る程度のジョブなら意識する必要もないでしょうが、かなりの時間をかけて集計してレコードを作っていくようなジョブの場合、ジョブの先頭で仮にすでに該当レコードが全部ないし一部存在するときは全て削除する、というようなことです。 ジョブはリトライしても同じ結果が得られるようにしましょう

ダッシュボードについて

f:id:attracie:20151018190216p:plain

Sidekiqにはダッシュボードがついてきます。待機状態というのは先ほど言ったプロセスに渡っていないRedisに積まれたジョブ、ビジーが現在実行中のジョブで処理完了・失敗に分類されていきます。(個人的には並びが逆のほうがわかりやすい気がします)

ビジーになっている現在実行中のジョブはスレッドベースで並列処理されますが、config/sidekiq.ymlで定義されたconcurrencyの数を超えることは(シングルプロセスの場合は)ありません。

※未検証ですがマルチプロセスだとプロセス数×concurrencyの数が上限になるかもしれません

queue(待機状態)はdefaultのほか、独自に定義して増やすことができます。 queueで分類することで溜まったジョブをまとめて消したり、処理の優先順位付けができます。

最初できるかなと思ったのですが、queueごとで処理のスレッド数を割り当てることは(標準では)できないようです。つまりconcurrencyが25のとき、5をheavy_task、20をdefaultに割り当てるというような使い方です。

ライブラリがあるので、もう少し込み入ったqueueの使い方をしたいときはこのあたりが必要になります。

github.com

まぁとはいえ、標準でサポートされていなければ実際なくてもなんとかなる機能な気もするので、私はこのgemの導入を見送りました。

connection切れ、楽観的ロック

重い処理を行うJobの場合、ActiveRecord絡みで注意することがあります。 一つはActiveRecordのconnectionが接続が切れた時です。

d.hatena.ne.jp

重い集計処理をしてからインサートする場合 インサート直前でActiveRecord::Base.clear_active_connections!を呼ぶと直ると思います。

と書いてあるように、

result = Misc.too_large_calculate #とても時間がかかる
Result.create!(result) # 接続が切れている

こういうコードは途中ActiveRecordのコネクションが切れるので失敗します。claer_active_connections!でもいいですが、

  def perform
    result = Misc.too_large_calculate #とても時間がかかる
    with_connection { Result.create!(result) }
  end

  def with_connection
    ActiveRecord::Base.connection_pool.with_connection do
      yield
    end
  end

このようにコネクションを指定することで回避できます。

次に楽観的ロックについてですが、

user = User.find(id)
result = Misc.too_large_calculate
user.update(result)

このようなコードは計算時間中にuserが別のプロセスによって更新されてupdated_atが書き換わると、問答無用にrollbackされます。なので最新状態のモデルを取得すればOKです。

user = User.find(id)
result = Misc.too_large_calculate
with_connection { user.reload.update(result) }

メモリの問題

sidekiqで画像をImageMagickで変更・保存する処理を実行していると、このようなエラーが頻出しました。

error Cannot allocate memory - identify

これはまだ原因がはっきりわかっていないのですが、この記事に書いてある問題に関係があるようです。

adamniedzielski.github.io

先ほどのエラーももとをたどるとMiniMagickがバッククオートでidentifyを呼んでいる箇所が怪しいので、どうやらSidekiqのスレッドからさらに子プロセスを発生されるようなコードを書くとかなりメモリを食う、ということかなと思っています。(このあたりはUNIXシステムコールやメモリについてしっかり理解しているほうが原因が分かりそうですね)

これはチューニングで解決できそうですが、とりあえずはメモリが4GBくらいのサーバでconcurrencyを3くらいに抑えて動かせば落ちないで済んだので、それで乗り切っています。

追記

さすがに4GBで3並列しかできないのはおかしいなと思っていろいろ見たところ、MiniMagickにはよりメモリ効率の良いposix-spawnによるプロセス生成が可能でした。

github.com

def execute(command)
      stdout, stderr, status =
        MiniMagick.logger.debug(command.join(" ")) do
          Timeout.timeout(MiniMagick.timeout) do
            send("execute_#{MiniMagick.shell_api.gsub("-", "_")}", *command)
          end
        end

      [stdout, stderr, status.exitstatus]
    rescue Errno::ENOENT, IOError
      ["", "executable not found: \"#{command.first}\"", 127]
    end

設定でshell_apiopen3posix-spawnで切り替え可能です。(標準はopen3です) posix-spawnはgemが必要なのでこれをGemfileでロードする(require: falseでOK)だけでメモリ効率が上がります。

システムコールに詳しくないので、なぜposix-spawnのほうがメモリ効率が良いのか、効率がいいならもともと標準でこちらを使っていないのはなぜなのかはよくわかりません。が、

https://github.com/minimagick/minimagick#errnoenomem

Errno::ENOMEM

It can happen that, when dealing with very large images, the process runs out of memory, and Errno::ENOMEM is raised in your code. In that case try installing the posix-spawn gem, and tell MiniMagick to use it when executing shell commands.

MiniMagick.configure do |config| config.shell_api = "posix-spawn" end

とあるように、Railsのプロセスが画像処理を行うと標準でRailsプロセスと同じだけのメモリを消費する子プロセスが生まれるので、Rails側でメモリに載せた画像やPDFのサイズが大きいとメモリがシビアに圧迫されるようです。

パスの問題

Sidekiqでconvertのようなコマンドが見つからないというエラーが出て、サーバに入ってconvertを叩いてみると問題なく見つかる、というようなエラーがあります。これはbashrcのパスや環境変数をSidekiqが共有していないために発生します。 対策として、bashrcの環境変数管理はやめて、capistranoでセットするようにしましょう。

paths = [
  '/usr/local/bin',
  '/usr/local/rvm/gems/ruby-2.1.1/bin',
  '/usr/local/rvm/gems/ruby-2.1.1@global/bin',
  '/usr/local/rvm/rubies/ruby-2.1.1/bin',
]

set :default_env, {
  path: "#{paths.join(':')}:$PATH",
  ld_library_path: '/usr/local/lib',
  ldflags: "'-L/usr/local/lib -Wl,-rpath,/usr/local/lib'"
}

デバッグ

以上のように、RailsのコードとSidekiqのコードではけっこう勝手が違う部分があるのでそれを意識しないと頻繁にエンバグします。 そこでsidekiqのワーカーの状況をRailsのモデル等にメモすることがオススメです。

例えばJob側のコードで

def perform
  Model.reload.update(status: "[#{current_time}] start")
  ...
  Model.reload.update(status: "[#{current_time}] end page #{n + 1} / #{length}")
  ...
rescue => e
  Model.reload.update(status: "[#{current_itme}] error : #{e.message}")
end

def current_time
   Time.current.strftime("%Y/%m/%d %H:%M:%S")
end

というふうにこまめに処理の状況を更新して、

f:id:attracie:20151018195851p:plain f:id:attracie:20151018195856p:plain

管理画面でこのように表示させるだけでぐっとわかりやすくなります。

まとめと雑感

以上みたように割りとSidekiqの導入はともかく、運用はめんどくさいことが多いです。私見ですがスタートアップだと本当に非同期処理じゃないといけないのか、何かうまく工夫してSidekiqを使わないで済む方法はないか考えたほうがいいことが多い気がします。

UICollectionViewのHeaderをxibから出し分ける

UICollectionView vs UITableView

UITableViewのtableHeaderViewのように、UICollectionViewの上端にheaderを入れたい時、collectionView:viewForSupplementaryElementOfKind:atIndexPath:というメソッドでsectionの先頭にヘッダーを追加すると、UITableViewとは違ってスクロール時に固定されることがない、tableHeaderViewのような見た目を作ることができます。

ただしこの方法で動的にheaderの出し分けを行いたいときは少し工夫が必要です。なぜならstoryboardではsectionに複数のviewを登録しておくことができないためです。

出し分けるview(UICollectionReusableView)をxibで定義する

xibで何種類かのheaderを作成しておきます。

f:id:attracie:20151009075311p:plain

このとき、特にこだわり・実装上の都合がなければ

  • 出し分けるxibの名前
  • (必要があれば)xibに対応するUICollectionReusableViewのサブクラス名
  • collectionViewのreusableIdentifier
  • UICollectionViewを管理するクラスで、どのヘッダーを使うかの識別子

を全て同じEnum(文字列)で表現するとわかりやすくなります。

ViewControllerのコード

class CollectionViewController : UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
    enum ListType : String {
        case Default = "CatalogListDefaultHeaderView"
        case Bookmark = "CatalogListBookmarkHeaderView"
        case Popular = "CatalogListPopularHeaderView"
        case Recent = "CatalogListRecentHeaderView"
    }

    var listType : ListType = .Default

    override func viewDidLoad() {
        super.viewDidLoad()

        for type in AllListType {
            let nib = UINib(nibName: type.rawValue, bundle: nil)
            collectionView.registerNib(nib, forSupplementaryViewOfKind: UICollectionElementKindSectionHeader, withReuseIdentifier: type.rawValue)
        }
        
         self.flowLayout.headerReferenceSize = CGSize(width: collectionView.frame.width, height: 100)
    }

    func collectionView(collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, atIndexPath indexPath: NSIndexPath) -> UICollectionReusableView {
        var reusableView : UICollectionReusableView!
        
        if kind == UICollectionElementKindSectionHeader {
            reusableView = collectionView.dequeueReusableSupplementaryViewOfKind(kind, withReuseIdentifier: listType.rawValue, forIndexPath: indexPath)
        }
        
        return reusableView
    }
}

全体像はざっとこうなるのですが、ポイントはいくつかあります。

qiita.com

だいたいこちらの記事に似たような内容があるのですが、

  • headerReferenceSizeに値を入れないとviewForSupplementary...が呼ばれないこと
  • viewForSupplementary...で登録されていないviewをいきなり返しても実行時エラーになること
  • 表示のタイプをString側のenumで定義しておくと管理が楽

というあたりがポイントです。

UICollectionViewはtableのように使えたり表現力がとても豊かで個人的に好きです。

rackアプリケーションでリビジョンを知らせてくれるrack-git-revision

動作中のRailsアプリケーションのバージョンを確認するためのrack middlewareとしてrack-git-revision というgemを作成しました。

何が嬉しいのか

本番環境にコードをデプロイしたときにそれが正しく反映されているのか、わかりづらいことが意外とあります。その原因として考えられるのが

  • アプリケーションサーバ複数台あるとき、一部のサーバのデプロイに失敗している
  • 本番環境のキャッシュ
  • RAILS_ENV=productionの時だけ発生する不具合(same origin policyなど、javascriptの問題がつまずきがちな気がします)
  • pushやマージを忘れたままデプロイコマンドを実行してしまった

などいろいろとあるのですが、反映をチェックするにはデプロイされているコードのgitのHEADのハッシュを確認することが個人的には多いです。

また、デプロイ権限者が複数いるときに今出ているバージョンがどこまでなのかを把握するためにも本番を直接確認したいことがあると思います。

そこで特定のURLを叩けばリビジョンが返るようにできると嬉しいわけです。

Rack Middlewareについて

ライブラリの紹介だけだと本文が短いので、ついでにRack Middlewareであることのメリットについても触れたいと思います。

そもそもRack Middlewareとは何かですが、このタマネギの層構造が一番有名です。

https://qiita-image-store.s3.amazonaws.com/0/4365/fb04e1b7-2049-0be2-d81f-eefe4e29d6af.png

リクエストが来てレスポンスが返るまでの一本道に、ゲートのようにrack middlewareがあってそれを通過するとリクエスト・レスポンスが加工したり、処理を変更したりできます。 Rackのcallメソッドが実装されたオブジェクトであればrack middlewareになれるようです。

rack middlewareにするメリットとして、場合によって処理を中断したり加工する際にRails本体に手をいれる前に処理が走ったり、Railsから独立して処理がこなせるので疎結合になってメンテナンスしやすいメリットがあるように思います。 ヘッダーに何かを追加するとか、Rails本体で発生する前にRack側で起きるエラーのハンドリングなどです。

一方でRailsアプリから独立していることが前提なので、middlewareからRailsMVCコードを参照したり、そもそもRailsでないと動かないようなコードを書くのはbad patternでしょう。

個人的な意見ですが、rack middlewareは今回のようなライブラリ製作には向くのですが、サービスコードを書く際に使うのは良くない気がします。

google compute engineでSSLを有効化するまで[Rapid SSL]

HTTPSが必要になったので、SSL証明書の購入、その有効化までをやってみました。

SSL証明書の購入

SSL証明書RapidSSLとかGlobalSignのような大手がその発行元になるのですが、購入自体はそれ以外の代理店からもできて、むしろ代理店経由のほうが一般的に安く手に入るようです。 いくつか見た中で私はRapidSSLワイルドカード版がほしいなと思ったので、それが安く手に入る代理店を探しました。 なぜRapidSSLかというと、他にもいろいろな認証局はありますが安い証明書というジャンルで一番知名度があるように思ったからです。微妙な言い回しですが、安さだけならもっとほかにも安いのはあったので、そっちを選択してもいいかもしれません。

購入はこちらから行いました。お名前comとか、管理画面めちゃくちゃうざいですがここのサイトは管理画面がbootstrapで組んであって、フランクで使いやすかったです。

www.ssl-store.jp

秘密鍵CSRを作成する

$ openssl genrsa -des3 -out server.key 2048
enerating RSA private key, 2048 bit long modulus
....................................................................................................................................................................................+++
..................................................................................+++
e is 65537 (0x10001)
Enter pass phrase for server.key:  //パスフレーズの入力
Verifying - Enter pass phrase for server.key: //確認

この秘密鍵をもとにCSRの作成を行います。このCSRは署名要求というもので、この署名要求をもとに認証局が証明書を発行するわけです。

$ openssl req -new -key server.key -out server.csr
Enter pass phrase for server.key:
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:JP //国名を入力
State or Province Name (full name) [Some-State]:Tokyo //都道府県を入力
Locality Name (eg, city) []:Minato  //市区町村を入力
Organization Name (eg, company) [Internet Widgits Pty Ltd]:XXX //会社名を入力
Organizational Unit Name (eg, section) []: //空エンターでOK
Common Name (e.g. server FQDN or YOUR name) []:*.domain.com //**最も重要。ワイルドカードの指定を忘れずに。**
Email Address []:admin@domain.com //連絡先を入力

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []: //空エンターでOK
An optional company name []: //からエンターでOK
$ cat server.csr //作成の確認
-----BEGIN CERTIFICATE REQUEST-----
asdfasdf
asdfasdf
-----END CERTIFICATE REQUEST-----

で、表示された文字列を先ほどのSSLstoreの管理画面から入力して、署名要求と(基本的に)同じ情報を入力して申請します。 ただ、ssl storeのフォームに署名要求の入力事項にないものもありました。

f:id:attracie:20151003191240p:plain

で、ドメインに間違いがないか確認して送信します。これで代理店がRapidSSL側のAPIか何かを叩くみたいで、geotrust(RapidSSLの会社)からメールが来ます。

f:id:attracie:20151003191712p:plain

メール中のURLをクリックするとgeotrust側の最終確認画面が出るので、間違いがないかチェックします。

f:id:attracie:20151003192248p:plain

承認すると5分〜10分ぐらいで代理店からメールで証明書が送られてきました。(メールで証明書送っていいのかな笑?)

この本文にはSSLサーバ証明書中間CA証明書の2つが含まれています。とりあえず実務上はこの2つがセットで必要なんだとおぼえておけば十分のようです。さらに先ほどCSRのために作成した秘密鍵もとっておく必要があります。 詳しい仕組みについては解説があるので参照してください。

Webセキュリティー入門: 第2回「さらに知りたいセキュリティー(SSL)の仕組み」

サーバのSSLを有効化する

サーバはapache2を使っている前提です。

$ sudo a2enmod ssl
$ sudo service apache2 restart
$ sudo nmap -p 443 localhsot
Host is up (0.000033s latency).
Other addresses for localhost (not scanned): 127.0.0.1
PORT    STATE SERVICE
443/tcp open  https

portが開放されていなければfirewallをチェックしてください。compute engineだとインスタンスの編集画面からhttpsを有効化するチェックボックスがあります。

パスフレーズを無効化する

先ほどパスフレーズ付きの秘密鍵を作っていますが、このままだとapache起動時に毎回パスフレーズの入力が必要なのでパスフレーズがないバージョンのものを作ってこちらを参照します。

$ cp secret.key secret_original.key
$ openssl rsa -in secret.key -out secret.key  //この生成のときにパスフレーズが求められる

apacheのconfで秘密鍵、中間証明書、証明書を指定する

apacheのバージョンによってここの指定方法は異なりますが、私のcompute engineに標準で入っていたバージョンは2.2.22でした。

$ sudo apachectl -v
Server version: Apache/2.2.22 (Debian)
Server built:   Dec 23 2014 22:48:32

apache2のバージョンによっては以下のconfファイルの指定方法はけっこう異なるようです。

qiita.com

<VirtualHost *:80>
  DocumentRoot /path/to/rails-app/public
  Redirect permanent / https://xxx.yyy.me/
</VirtualHost>

<VirtualHost *:443>
  SSLEngine on
  SSLCertificateFile /path/to/server.crt //証明書
  SSLCertificateKeyFile /path/to/secret.key //パスフレーズなしの秘密鍵
  SSLCertificateChainFile /path/to/chain.crt //中間証明書
  SetEnv SECRET_KEY_BASE secret
  DocumentRoot /path/to/rails-app/public 
</VirtualHost>

あとはapacheをリロードすればhttpsで接続可能になりました。上記のリダイレクトはルートはもちろん、サブディレクトリへのアクセスも適切にリダイレクトしてくれます。 (例) http://xxx.yyy.me/hoge => https://xxx.yyy.me/hoge

f:id:attracie:20151003201755p:plain

NSObjectを継承した独自クラスをNSUserDefaultsに保存する[Swift]

iPhoneアプリで手軽にローカルにデータを保存する際にNSUserDefaultsが重宝します。

ただ、通常の方法だと単純なデータ(文字列、Dictionaryなど)を格納することはできるのですが、NSObjectを継承して作ったような独自クラスの保存はできません。 NSUserDefaultsで扱えるようにするにはNSCodingというプロトコルに応答することと、それをアーカイブして保存する2つの工程が必要です。

1. NSCodingを実装する

import Foundation
import SwiftyJSON

class PhotoModel: NSObject, NSCoding {
    var id : Int?
    var picture : Dictionary<String, AnyObject>?
    
    class func photosFromJSON(list : Array<JSON>) -> [PhotoModel] {
        var array = [PhotoModel]()
        for item in list {
            let photo = PhotoModel()
            photo.assignAttributes(item)
            array.append(photo)
        }
        return array
    }
    
    func assignAttributes(params : JSON) {
        self.id = params["id"].intValue
        self.picture = params["picture"].dictionaryObject
    }
    
    func encodeWithCoder(aCoder: NSCoder) {
        aCoder.encodeInteger(id!, forKey: "id")
        aCoder.encodeObject(picture, forKey: "picture")
    }
    
    required init(coder aDecoder: NSCoder) {
        id = aDecoder.decodeIntegerForKey("id")
        picture = aDecoder.decodeObjectForKey("picture") as? [String : AnyObject]
        super.init()
    }
    
    override init() {
        super.init()
    }
}

これはAPIサーバから受け取ったJSONを処理してモデルを表現する簡単なクラスですが、encodeWithCoder(aCoder: NSCoder)init(coder aDecoder: NSCoder)の2つを実装すればいいわけです。

ただ、init(coder aDecoder: NSCoder)の他にinit()も残しておいたほうがPhotoModel()のような単純なイニシャライズができるので書いておきましょう。

2. NSKeyedArchiverで保存、NSKeyedUnarchiverで取り出す

次にエンコード可能なオブジェクトをアーカイブしてNSData型にして格納します。ご覧のように格納、受け取るオブジェクト自体はArray型([PhotoModel])ですが、userDefaultsで扱う際にはNSDataなのでarrayForKey:でなくobjectForKeyでOKです。

    func userDefault() -> (NSUserDefaults) {
        return NSUserDefaults.standardUserDefaults()
    }

    func setDraftPhotos(photos : [PhotoModel]) {
        let archive = NSKeyedArchiver.archivedDataWithRootObject(photos)
        userDefault().setObject(archive, forKey: "draftPhotos")
    }

    func draftPhotos() -> ([PhotoModel]) {
        if let data = userDefault().objectForKey("draftPhotos") as? NSData {
            let unarchive = NSKeyedUnarchiver.unarchiveObjectWithData(data)
            return unarchive as! [PhotoModel]
        } else {
            return [PhotoModel]()
        }
    }

まとめ

これはあくまでクライアント型で2,3個のデータを保存したい際に便利な手法です。 もっと大規模に保存するならCoreDataの使用を検討しましょう。

また、モデル自体を保存するよりもid配列を保存して実行のたびにサーバからロードするほうがいい場合もあるのでサービスの要件と相談すべきです。