不格好エンジニア

wordpress.comから引っ越しました。

仕事を楽しくする【AWSを使わない】はてぶの記事をサムネイル付きでJSON配信する3ステップ

はじめに

類似するネタは、オンライン上に色々な方が書いて下さっているのですが、"サムネイルはAWS S3に保存しよう!"的なネタが多く、VPS一台で処理を完結したい自分にとっては、そのものズバリの記事がありませんでした。需要があるかどうかわかりませんが、書いてみたいと思います。

おおまかに実装方針を立てると、このようになります。

  • はてぶのAPIから記事を収集する(XMLをフェッチしてパース)
  • はてぶの記事URLからサムネイルを生成してDBに保存(dragonflyもしくはcarrierwaveを用います。どちらでも可能です)
  • 記事とサムネイルをJSON形式で配信する

今回は主要部分を抜粋してご紹介します。ソースコードはこちらに公開しております。

tjnet/geeknews-rails · GitHub

(1)はてぶのAPIから記事を収集する(XMLをフェッチしてパース)

まずはとにかく記事データが必要ということで、railsを用いて記事を収集します。データ収集に利用したのは以下のコードです。

#coding: utf-8
require 'pp'
require 'net/http'
require 'uri'
require 'rexml/document'

module Tasks
  class ArticlePicker
    # initialize article data
    def self.execute
      ActiveRecord::Base.connection.execute("TRUNCATE TABLE articles")
  
      feed_list = Feed.all
      articles = []
  
      feed_list.each do |feed|
        next if feed.url.blank?
        articles = fetch_and_parse(feed.url)
        save_articles(articles, feed.category_id)
      end
    end
  
    # save articles
    def self.save_articles(articles, category_id)
      return if articles.empty?
      pp '--'
      pp Time.now
  
      articles.each do |article|
      #pp sprintf('save %s of %d', article['title'], category_id)
      pp sprintf('save %s of %d', article['link'], category_id)

        Article.create!(
          :category_id => category_id,
          :title => article['title'], 
          :link => article['link'], 
          :description => article['description'], 
        )

      end
    end
  
    # return parsed articles
    def self.fetch_and_parse(url)
      res = fetch(url)
      articles = xml_to_array(res)
      pp articles
      articles
    end
  
  
    # fetch response from url
    def self.fetch(url)
      pp URI.escape(url)
  
      uri = URI.parse URI.escape(url)
      res = Net::HTTP.get uri
  
      res
    end
  
    # return parsed XML
    def self.xml_to_array(xml)
      items_rx = []
      doc = REXML::Document.new xml
      doc.elements.each('//item') do |e|
        i = Hash.new
        i["title"] = e.elements['title'].text
        i["link"] = e.elements['link'].text
        i["description"] = e.elements['description'].text
        items_rx << i
      end
  
      return items_rx
  
    end
  
  end
end

リトライ処理などは、別のスクリプトに切り出しています。

    begin
      pp "updating article ..."
      Tasks::ArticlePicker.execute
    rescue Exception => e
      pp "[ERROR] update_article #{e.message}"
      retry_count += 1
      retry if retry_count <= 5
      pp "retried 5 times so exit"
      exit
    end
    pp "updated article"

(2)はてぶの記事URLからサムネイルを生成してDBに保存(dragonflyもしくはcarrierwaveを用います。どちらでも可能です)

URLからサムネイルを作成するためにwkhtmltoimageを実行しています。
DBへの保存はcarrierwaveを用いています。

   # Generate and assign an image or set a validation error
    begin
      tempfile = temp_thumbnail_path
      cmd = "wkhtmltoimage --quality 40 --width 50 --height 50 \"#{self.link}\" \"#{tempfile}\""
         p "*** grabbing thumbnail: #{cmd}"
      system(cmd) # sometimes returns false even if image was saved
      
      self.photo = File.new(tempfile) # will throw if not saved
      self.save
    rescue => e
         p "*** thumbnail error: #{e}"
      self.errors.add(:base, "Cannot generate thumbnail. Is your URL valid?")
    ensure
    end

DBへのデータ保存が終われば、wkhtmltoimageで生成した一時ファイルは不要となりますので、modelのコールバックで後始末します。

 #Cleanup temp files when we are done
  after_save :cleanup_temp_thumbnail
   
  # Cleanup the temporary thumbnail image
  def cleanup_temp_thumbnail
    File.delete(temp_thumbnail_path) rescue 0
  end

(3)記事とサムネイルをJSON形式で配信する

ここまででDBに画像データを保存できました。後は、これをcontrollerからJSONとして配信します。

class Api::V1::ArticleController < ApplicationController
 # return articles
 def index
   render json: Article.list(request)
 end
end
 # 
 #= return articles as array of hash
 #
 def self.list(request)
   res = Array.new
   Article.all.each do |article|
     res.push({
       :category_id => article.category_id,
       :title => article.title,
       :link => article.link,
       :description => article.description,
       :image_url =>(article.photo.blank?) ? '' :  "#{request.protocol}#{request.host_with_port}#{article.photo.url}",
     })
   end
   res
 end

記事URLを生成するために、controllerからrequestを受け取っています。
もう少し簡潔に書けそうですが。。。

はまりどころ

うまく動かないときは、このあたりをチェックしてみて下さい。

  • サムネイルを一時的に保存するディレクトリのパーミッションなど
  • public/以下のディレクトリ構造
  • 本番環境だけで動かないときは、デプロイ設定やrackサーバの再起動が正常になされているか

TODO

次は、このあたりに取り組みたいですね。

  • cronの設定
  • サムネイルのクオリティを向上させる
  • 静的ファイルの配信を高速化

【VPSか、AWSか、Herokuか】結局、最後はさくらVPS+Unicorn+Rails+Capistranoに行き着いた【構築スクリプト付き】

お仕事以外にも色々手を出しています。
主に知り合いの方とサービス作ってみて頓挫したり、細々とiPhoneアプリを開発してみたり、、まったり作っていきたいと思います。

サーバを用意する際の選択肢について

自分のサービスやアプリを公開するとなった場合、どうしてもサーバが必要になります(僕の場合、自前のAPIと連携しないアプリって何となくモチベーションが湧かないのです。もちろん、既存のAPIを叩くだけでも、アプリを作成する事は可能ですが、はてなブックマークの記事を取得するだけのアプリでも、後からレコメンド機能を実装したり、記事を独自アルゴリズムで分類したり、、、やってみたいですよね。)

現実的な選択肢

みなさん、どんな観点でプラットフォームを選択されてるんでしょう。
ここでは、3つの観点から考えてみました。

  • 学習コスト
  • 技術的制約
  • ランニングコスト

学習コスト

まずAWSは、AWSそのものの作法を覚えなければならない為、(ベースになるLinuxの知識+AWSの知識)が必要となり、学習コストは最も高そうです。
さくらのVPSに関しては、色んな方が構築方法を公開されていて情報量も多く、ピュアなLinux単体の上に構築していくため、AWSと比較すれば、把握する範囲は比較的小さくなります。一定の水準でセキュリティを保つ設定が出来ていれば、なんとかなりそうですね。流れとしては、いったんは手動でLAMP環境を作ってはサーバを初期化して、、、といった事を繰り返してみるのが勉強になった気がしています。SSHの設定を間違えてログインできなくなり、泣く泣くサーバを初期化したのも、今となっては良い思い出。さすがに今は忙しくなって来たので、Ansibleで構築手順を半自動化しています。

AWSVPSでサービス開発を行う場合、手間がかかりそうな所といえば、デプロイ環境の構築でしょうか。手元のMacで開発=>本番のさくらVPSにデプロイ、といった構成にしたい場合は、自分でこれを設定しなければなりません。

セキュリティの設定、デプロイツールのセットアップをすっとばしたい、という場合はHerokuでしょうか。
デプロイに関しては、Herokuは圧倒的に簡単です。
手元のMacで開発したプロジェクト配下で"git push heroku master"とかやれば、デプロイ完了です。最も初速の出やすい開発プラットフォームではないでしょうか。

技術的制約

今回は、ニュースアプリの作成を予定しているため、サーバにサムネイルを一時保存したかったのですが、Heroku単体ではそれができない為、色々検証した結果、Herokuは選択肢から外しました。
次にAWS。スタートアップ界隈では実質業界標準になってきている(気がする)AWSには、正直何でも搭載されている感があります。スケールさせやすかったり、Herokuで出来ないファイル保存ができたり、、。ただ、趣味の開発でここまで必要なのか。。と言われると正直必要ない場面も多そうですね。色々有りすぎるために、把握しないといけない範囲が増えて手が止まるのは嫌ですね。
さくらのVPSは、AWSみたいにポチポチインスタンス増やしてスケールさせたりは出来ません。ただ、ピュアなLinuxであり、root権限も付与されているため、1台にNginx+(Unicorn+Rails)+MySQL全部盛り、みたいな構成にするならVPSで十分ですね。

ランニングコスト

ちょっとした検証用のWebアプリケーションであれば、Herokuで十分な事もあるかもしれません。
ただ、DBは無料で使用できるレコード件数に上限があったり、重めのバッチ処理を走らせると課金されてたり、意外と制約が多いな、、、という印象です。というより、少し課金体系がわかりにくいです。
また、AWSに関しては、「いつのまにか結構課金されとる。。」という状態に陥りがちなので、サービスがスケールされる道筋が見えていない現状では選択肢から外しました(インスタンス立ち上げっぱなしで忘れてた自分が悪いのですが)。
さくらのVPSは月額定額なので、わかりやすさでは一番ですね。

開発環境/本番環境を構築する上での前提

  • 手元のMacが壊れても、開発環境を再現できる
  • 本番環境(さくらVPS)の設定はすべてAnsibleを通す

というあたりを意識して作りました。個人開発なら、Ansibleで自動化するのが、おすすめですよ。

開発環境

Ansibleを用いてvagrant上に(CentOS+Unicorn+Rails)の環境を構築します。サーバサイドの開発はVagrant上で完結します。
こちらの記事なども、参考になりそうです^^

【5分で学べる】Vagrant上にRailsをAnsibleでかんたんクッキング(CentOS6, MySQL, Rails4, Unicorn) - 不格好エンジニア (引っ越しました)

サーバ設定などのアップデート

開発環境/本番環境を問わず、すべてAnsibleを通して行っています。実際にCentOS+MySQL+Unicorn+Railsを導入するための構築スクリプトは、こちらに公開しています。

tjnet/vagrant_sakuravps_rails · GitHub

本番へのデプロイ

Railsプロジェクトのリポジトリにデプロイスクリプトを入れています。具体的にはCapistrano3(capistrano3-unicorn)を導入しています。
実際にはAnsibleでもデプロイは出来るのですが、Capistranoの方がRailsとの相性は良さそうです。staginやproductionといった複数環境へのデプロイに対応していたり、bundle、migration、unicornとの連携も考慮されており、使いやすいと思います。

作成中のサービスにCapistranoを導入した事例はこちら(config/deploy.rbのあたりは見よう見まねで、おかしい所が多々あるかもしれません)。


tjnet/geeknews-rails · GitHub


デプロイ設定をGitHubで公開するかどうかは、少し悩みました。ただ、ローカル(vagrant)のHostsファイルに本番環境のIPアドレスを記述するようにすれば、具体的なサーバの情報をGitHubに公開してしまう事もありません。アドバイスを頂けるチャンスもあるかもしれないですし、こんな情報でも誰かの役に立つかもしれないので、積極的にシェアしていきたいと考えています。

かかるコスト

かかるコストですが、シンプルに使ったサーバ台数x使用したVPSの月額料金です。

今後やりたいこと

  • CI環境の構築
  • TDD (後からでもテスト書こう)
  • 監視ツールの導入

今の構成がベストではないと思うので、まだまだ改善したいと思います。日々、勉強ですね!

【怠惰な】Swiftを学ぶにあたって最適なオープンソース【Objective-C経験者向け】

筆者のスペック

業務やプライベートプロジェクトでObjective-C を1年半ほど、経験しました。最近、サンプルコードやライブラリもSwiftを用いたものが増えて来て、少し焦っています。Swift勉強しなくちゃ。

困っていること

チュートリアルをチマチマ進めていくのが性に合わない

Railsチュートリアルは非常によく出来ていて、一連のストーリーが有ったので、進めていくのは苦痛ではありませんでした。ただ、Swiftに関しては、既にObjective-Cを経験したエンジニアであれば、いきなり小規模なアプリのソースコードをリファレンス引きながら読み進めて行く方が理解が早そうですね。

共有されているサンプルアプリが色々動かなくなっている

Swiftが発表され話題を集めた頃、ギークな方達が、たくさんのサンプルアプリをGitHubに晒して下さいました。ところが、これらのGitHubで共有されているコードがXcode6.1で動作しない事がよくあります。

学習に適したアプリ

とりあえず、いくつか試してみて、筆者の環境(Xcode6.1)で動作確認できたソースはこちら。


amitburst/HackerNews · GitHub

HackerNewsは、Objective-Cの教材としてもお世話になったプロダクトです。

Newsアプリは、内部動作も把握しやすいと思います。業務に応用できる部分も多いかと思いますので、こちらを読み進めていきます。

ちょっと読んでみた


AppDelegate.swiftに目を通してみます。

import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    
    // MARK: Properties
    
    var window: UIWindow?

    // MARK: UIApplicationDelegate
    
    func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: NSDictionary?) -> Bool {
        return true
    }

}

@UIApplicationMainの@は意味がわかりませんが、いったん後回しにしても良さそうです。

    var window: UIWindow?

この部分はプロパティ宣言ですね。後ろについている?は、nilを許容するかどうかを明示的に示しているそうです。このあたり、後で調べておきます。

    func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: NSDictionary?) -> Bool {
        return true
    }

この部分は、下のObjective-Cソースコードと比較すれば、何となく理解できますね。問題なさそう。

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.
    return YES;
}

AppDelegateは、何となくわかりました。
この調子で、読み進めていけそうです。

【あの有名サービスが】コードリーディングで学ぶAngularJS【丸裸に】

コードリーディング

AngularJS初心者です。
コードリーディングという形で、AngularJSで実装されたWebサービスソースコードを題材に使って、その動きや機能がどのように実装されているのかを、ソースコードのレベルで解析していこうと思います。

※時間のある時に、随時追記していきます。

題材「FMTube 」について

コードリーディングの題材とするのはこちら。
ゆーすけべーさんが、AngularJSの勉強中に作成したサービスだそうです。

FMTube!

アーティストの名前を入れると、楽曲リストを順番に再生してくれます。
素晴らしいです。勉強させて頂きます。

ソースコード

ソースコードですが、Webサービスとして公開しているので、当然、Webブラウザから確認することができます。
メインロジックは、こちらに記述されています。

http://yusukebe.github.io/FMTube/js/app.js

参考資料

正直、全然わからない。。。この辺りを参考にしながら進めていきます。

初心者向けAngularJS - その1 - albatrosary's blog

Service, Factory, Providerが何の事か分からない方はこちら。

AngularJs Service,Factory,Providerなどなど - senta.me/blog

エントリポイント

var app = angular.module('fmtube', ['ng']);

ここでは、このアプリケーションを初期化して、依存するモジュールを登録します。

view

次に、view部分をざっと眺めます。
#content, #listあたりに注目すれば処理内容を把握できそうです。

<html ng-app="fmtube" ng-controller="controller" class="ng-scope"><head><style type="text/css">@charset "UTF-8";[ng\:cloak],[ng-cloak],[data-ng-cloak],[x-ng-cloak],.ng-cloak,.x-ng-cloak,.ng-hide{display:none !important;}ng\:form{display:block;}.ng-animate-start{border-spacing:1px 1px;-ms-zoom:1.0001;}.ng-animate-active{border-spacing:0px 0px;-ms-zoom:1;}</style>
    <meta charset="utf-8">
    <title ng-bind="title" class="ng-binding">Eis essen by GReeen - FMTube!</title>
    <link href="css/bootstrap.min.css" rel="stylesheet">
    <link href="css/typeahead.js-bootstrap.css" rel="stylesheet">
    <link href="css/app.css" rel="stylesheet">
    <!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
    <!--[if lt IE 9]>
      <script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
      <script src="https://oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script>
    <![endif]-->
    <script id="www-widgetapi-script" src="https://s.ytimg.com/yts/jsbin/www-widgetapi-vflAiKzF-/www-widgetapi.js" async=""></script><script src="http://www.youtube.com/iframe_api"></script><script id="twitter-wjs" src="http://platform.twitter.com/widgets.js"></script><script async="" src="//www.google-analytics.com/analytics.js"></script><script src="js/jquery.js"></script>
    <script src="js/typeahead.min.js"></script>
    <script src="js/bootstrap.min.js"></script>
    <script src="js/angular.min.js"></script>
    <script src="js/angular-resource.min.js"></script>
    <script src="js/app.js"></script>
    <script>
      (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
      (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
      m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
      })(window,document,'script','//www.google-analytics.com/analytics.js','ga');
      ga('create', 'UA-492497-67', 'yusukebe.github.io');
      ga('send', 'pageview');
    </script>
  </head>
  <body style="" data-twttr-rendered="true">
    <div class="container">
      <div id="header" class="clearfix">
        <h1><a href="http://yusukebe.github.io/FMTube/">FMTube!</a></h1>
      </div>
      <!-- /header -->
      <div class="row">
        <div id="form" class="col-md-4">
          <form role="form" ng-submit="submit(true)" class="ng-valid ng-dirty">
            <div class="form-group">
              <span class="twitter-typeahead ng-valid ng-dirty" style="position: relative; display: inline-block; direction: ltr;"><input class="tt-hint" type="text" autocomplete="off" spellcheck="off" disabled="" style="position: absolute; top: 0px; left: 0px; border-color: transparent; box-shadow: none; background: none 0% 0% / auto repeat scroll padding-box border-box rgb(255, 255, 255);"><input type="text" class="form-control typeahead tt-query" placeholder="Artist name" ng-model="artist" autocomplete="off" spellcheck="false" dir="auto" style="position: relative; vertical-align: top; background-color: transparent;"><span style="position: absolute; left: -9999px; visibility: hidden; white-space: nowrap; font-family: 'Alegreya Sans', sans-serif; font-size: 14.2857141494751px; font-style: normal; font-variant: normal; font-weight: 400; word-spacing: 0px; letter-spacing: 0px; text-indent: 0px; text-rendering: auto; text-transform: none;">GReeen</span><span class="tt-dropdown-menu" style="position: absolute; top: 100%; left: 0px; z-index: 100; display: none; right: auto;"><div class="tt-dataset-artist" style="display: block;"><span class="tt-suggestions" style="display: block;"><div class="tt-suggestion" style="white-space: nowrap; cursor: pointer;"><p style="white-space: normal;">Greeen Linez</p></div><div class="tt-suggestion" style="white-space: nowrap; cursor: pointer;"><p style="white-space: normal;">GReeen</p></div><div class="tt-suggestion" style="white-space: nowrap; cursor: pointer;"><p style="white-space: normal;">Greeen</p></div><div class="tt-suggestion" style="white-space: nowrap; cursor: pointer;"><p style="white-space: normal;">Dr.Greeen</p></div><div class="tt-suggestion" style="white-space: nowrap; cursor: pointer;"><p style="white-space: normal;">Al Greeen</p></div></span></div></span></span>
              <button type="submit" class="btn btn-default"><b>Play!</b></button>
            </div>
          </form>
        </div>
      </div>
      <div id="social-buttons">
        <span style="margin-right:4px;">
          <iframe class="hatena-bookmark-button-frame" title="このエントリーをはてなブックマークに追加" frameborder="0" scrolling="no" width="130" height="20" src="javascript:false" style="width: 111px; height: 20px;"></iframe><script type="text/javascript" src="http://b.st-hatena.com/js/bookmark_button.js" charset="utf-8" async="async"></script>
        </span>
        <iframe id="twitter-widget-0" scrolling="no" frameborder="0" allowtransparency="true" src="http://platform.twitter.com/widgets/tweet_button.d58098f8a7f0ff5a206e7f15442a6b30.en.html#_=1416154421769&amp;count=horizontal&amp;id=twitter-widget-0&amp;lang=en&amp;original_referer=http%3A%2F%2Fyusukebe.github.io%2FFMTube%2Findex.html&amp;size=m&amp;text=FMTube!&amp;url=http%3A%2F%2Fyusukebe.github.io%2FFMTube%2F" class="twitter-share-button twitter-tweet-button twitter-share-button twitter-count-horizontal" title="Twitter Tweet Button" data-twttr-rendered="true" style="width: 109px; height: 20px;"></iframe>
        <script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0],p=/^http:/.test(d.location)?'http':'https';if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src=p+'://platform.twitter.com/widgets.js';fjs.parentNode.insertBefore(js,fjs);}}(document, 'script', 'twitter-wjs');</script>
        <span style="margin-left:-20px;">
          <iframe src="//www.facebook.com/plugins/like.php?href=http%3A%2F%2Fyusukebe.github.io%2FFMTube%2F&amp;width=150&amp;layout=button_count&amp;action=like&amp;show_faces=true&amp;share=true&amp;height=21&amp;appId=640667512644377" scrolling="no" frameborder="0" style="border:none; overflow:hidden; height:21px; width:150px;" allowtransparency="true"></iframe>
        </span>
      </div>

      <hr>

      <div class="row">
        <div id="content">
          <iframe id="player" frameborder="0" allowfullscreen="1" title="YouTube video player" width="600" height="400" src="https://www.youtube.com/embed/5sveP1gSXGA?autoplay=1&amp;rel=0&amp;enablejsapi=1&amp;origin=http%3A%2F%2Fyusukebe.github.io"></iframe>
        </div>
        <div id="list">
          
          <!-- ngRepeat: track in tracks --><div class="list-item ng-scope" ng-repeat="track in tracks">
            <a class="clearfix list-active" ng-click="click($index)" ng-class="active_class($index)">
              <img ng-src="" class="pull-left">
              <b class="pull-left ng-binding">Eis essen</b>
            </a>
          </div><!-- end ngRepeat: track in tracks --><div class="list-item ng-scope" ng-repeat="track in tracks">
            <a class="clearfix" ng-click="click($index)" ng-class="active_class($index)">
              <img ng-src="http://userserve-ak.last.fm/serve/34s/97541019.jpg" class="pull-left" src="http://userserve-ak.last.fm/serve/34s/97541019.jpg">
              <b class="pull-left ng-binding">Gesundes Ego</b>
            </a>
          </div><!-- end ngRepeat: track in tracks --><div class="list-item ng-scope" ng-repeat="track in tracks">
            <a class="clearfix" ng-click="click($index)" ng-class="active_class($index)">
              <img ng-src="" class="pull-left">
              <b class="pull-left ng-binding">Eis essen - JuliensBlogContest</b>
            </a>
          </div><!-- end ngRepeat: track in tracks --><div class="list-item ng-scope" ng-repeat="track in tracks">
            <a class="clearfix" ng-click="click($index)" ng-class="active_class($index)">
              <img ng-src="http://userserve-ak.last.fm/serve/34s/92538915.jpg" class="pull-left" src="http://userserve-ak.last.fm/serve/34s/92538915.jpg">
              <b class="pull-left ng-binding">Ein gesundes Ego</b>
            </a>
          </div><!-- end ngRepeat: track in tracks --><div class="list-item ng-scope" ng-repeat="track in tracks">
            <a class="clearfix" ng-click="click($index)" ng-class="active_class($index)">
              <img ng-src="http://userserve-ak.last.fm/serve/34s/92538915.jpg" class="pull-left" src="http://userserve-ak.last.fm/serve/34s/92538915.jpg">
              <b class="pull-left ng-binding">Nur das Beste</b>
            </a>
          </div><!-- end ngRepeat: track in tracks --><div class="list-item ng-scope" ng-repeat="track in tracks">
            <a class="clearfix" ng-click="click($index)" ng-class="active_class($index)">
              <img ng-src="http://userserve-ak.last.fm/serve/34s/92538915.jpg" class="pull-left" src="http://userserve-ak.last.fm/serve/34s/92538915.jpg">
              <b class="pull-left ng-binding">Die Blüte meiner Selbst</b>
            </a>
          </div><!-- end ngRepeat: track in tracks --><div class="list-item ng-scope" ng-repeat="track in tracks">
            <a class="clearfix" ng-click="click($index)" ng-class="active_class($index)">
              <img ng-src="http://userserve-ak.last.fm/serve/34s/92538915.jpg" class="pull-left" src="http://userserve-ak.last.fm/serve/34s/92538915.jpg">
              <b class="pull-left ng-binding">Blauer Planet</b>
            </a>
          </div><!-- end ngRepeat: track in tracks --><div class="list-item ng-scope" ng-repeat="track in tracks">
            <a class="clearfix" ng-click="click($index)" ng-class="active_class($index)">
              <img ng-src="" class="pull-left">
              <b class="pull-left ng-binding">JottBeBe Qualifikation</b>
            </a>
          </div><!-- end ngRepeat: track in tracks --><div class="list-item ng-scope" ng-repeat="track in tracks">
            <a class="clearfix" ng-click="click($index)" ng-class="active_class($index)">
              <img ng-src="http://userserve-ak.last.fm/serve/34s/92538915.jpg" class="pull-left" src="http://userserve-ak.last.fm/serve/34s/92538915.jpg">
              <b class="pull-left ng-binding">Hovercraft</b>
            </a>
          </div><!-- end ngRepeat: track in tracks --><div class="list-item ng-scope" ng-repeat="track in tracks">
            <a class="clearfix" ng-click="click($index)" ng-class="active_class($index)">
              <img ng-src="" class="pull-left">
              <b class="pull-left ng-binding">Lächeln Exklusiv</b>
            </a>
          </div><!-- end ngRepeat: track in tracks --><div class="list-item ng-scope" ng-repeat="track in tracks">
            <a class="clearfix" ng-click="click($index)" ng-class="active_class($index)">
              <img ng-src="" class="pull-left">
              <b class="pull-left ng-binding">Hippie 2.0</b>
            </a>
          </div><!-- end ngRepeat: track in tracks --><div class="list-item ng-scope" ng-repeat="track in tracks">
            <a class="clearfix" ng-click="click($index)" ng-class="active_class($index)">
              <img ng-src="" class="pull-left">
              <b class="pull-left ng-binding">Scheiss doch einfach drauf (feat. Der Rote)</b>
            </a>
          </div><!-- end ngRepeat: track in tracks --><div class="list-item ng-scope" ng-repeat="track in tracks">
            <a class="clearfix" ng-click="click($index)" ng-class="active_class($index)">
              <img ng-src="" class="pull-left">
              <b class="pull-left ng-binding">Hab Wörter für dich</b>
            </a>
          </div><!-- end ngRepeat: track in tracks --><div class="list-item ng-scope" ng-repeat="track in tracks">
            <a class="clearfix" ng-click="click($index)" ng-class="active_class($index)">
              <img ng-src="" class="pull-left">
              <b class="pull-left ng-binding">JottBeBe Winin Battle</b>
            </a>
          </div><!-- end ngRepeat: track in tracks --><div class="list-item ng-scope" ng-repeat="track in tracks">
            <a class="clearfix" ng-click="click($index)" ng-class="active_class($index)">
              <img ng-src="" class="pull-left">
              <b class="pull-left ng-binding">Vaubetee Quali</b>
            </a>
          </div><!-- end ngRepeat: track in tracks --><div class="list-item ng-scope" ng-repeat="track in tracks">
            <a class="clearfix" ng-click="click($index)" ng-class="active_class($index)">
              <img ng-src="" class="pull-left">
              <b class="pull-left ng-binding">Pinselstrich für die Ohren</b>
            </a>
          </div><!-- end ngRepeat: track in tracks --><div class="list-item ng-scope" ng-repeat="track in tracks">
            <a class="clearfix" ng-click="click($index)" ng-class="active_class($index)">
              <img ng-src="" class="pull-left">
              <b class="pull-left ng-binding">Abseits der Norm</b>
            </a>
          </div><!-- end ngRepeat: track in tracks --><div class="list-item ng-scope" ng-repeat="track in tracks">
            <a class="clearfix" ng-click="click($index)" ng-class="active_class($index)">
              <img ng-src="" class="pull-left">
              <b class="pull-left ng-binding">Ich bin hier</b>
            </a>
          </div><!-- end ngRepeat: track in tracks --><div class="list-item ng-scope" ng-repeat="track in tracks">
            <a class="clearfix" ng-click="click($index)" ng-class="active_class($index)">
              <img ng-src="" class="pull-left">
              <b class="pull-left ng-binding">Frauen</b>
            </a>
          </div><!-- end ngRepeat: track in tracks --><div class="list-item ng-scope" ng-repeat="track in tracks">
            <a class="clearfix" ng-click="click($index)" ng-class="active_class($index)">
              <img ng-src="" class="pull-left">
              <b class="pull-left ng-binding">VauBeTee Qualifikation - Instrumental</b>
            </a>
          </div><!-- end ngRepeat: track in tracks -->
        </div>
      </div>
      
      <hr>
      <div id="footer">
        <address>
          © <a href="http://yusukebe.github.io/">yusukebe</a>,
          <b>FMTube</b> using Last.fm Web API, YouTube API, jQuery, AngularJS, Twitter Bootstrap and GitHub!
        </address>
      </div>
    </div>
</body></html>

run

またjs/app.jsに戻って、処理内容を確認します。

app.run(function(){
  var tag = document.createElement('script');
  tag.src = "http://www.youtube.com/iframe_api";
  var first_tag = document.getElementsByTagName('script')[0];
  first_tag.parentNode.insertBefore(tag, first_tag);
});

runって何ですか。。。?
StackOverflowさんによると、constantsやinstanceはrunブロックでしか注入できないらしい。

[参考]

breeze - AngularJS app.run() documentation? - Stack Overflow

Factory, Service, Providerについて

Service,Value,Factory,Provider ってなんぞ?そこについて前提知識がないと、以降のコードを理解するのは難しそうです。

先ず、サービスとは…
Angularサービスはシングルトンオブジェクト、またはWebアプリケーション共通の特定のタスクを実行する関数です。 Angularは、サーバにリクエストを送るブラウザのXMLHttpRequestオブジェクトにアクセスする$httpサービスのような、 いくつかの組み込みサービスを持ちます。 他のコアなAngular変数と識別子と同様に、組み込みのサービスは常に$から始まります。(前述した$httpのように) また、独自のカスタムサービスを作ることも可能です。
とのこと。 (http://js.studio-kingdom.com/angularjs/guide/understanding_services)
このサービスをDIに登録する方法が複数あり、大分とまどいました。

[参考]
AngularJs Service,Factory,Providerなどなど - senta.me/blog

DI

この際、ぶっちゃけると「DIとか何それおいしいの?」な人もたくさんいらっしゃる事でしょう。
僕もその1人です。こちらを読むと何となくわかった気になれます。


要するに DI って何なのという話 - 猫型の蓄音機は 1 分間に 45 回にゃあと鳴く


明日から、ドヤ顔で新人君に説明できますね。どや!

今日はここまで。また追記します。。。

Unicorn(Rackサーバ)+Nginx(リバースプロキシ)の構成が多用されている理由について、調べてみた話

使ってますか? Unicorn

Unicornは本番環境用に広く使われているRackサーバの一つです。位置づけとしてはMongrelやThinに近いものです。お仕事ではPhusion Passengerを使用していますが、今回はUnicornについて学んだ事をまとめてみました。

Unicornの特徴

  • 高速なクライアントからのレスポンスに対し、短時間の処理をすばやく返す
  • 運用コストが低い(Rainbows!やPumaと比較して枯れており、ノウハウが充実している)

ここでいう「高速なクライアント」とは、帯域をめいっぱい使えるクライアントの事です。
例えば、LAN上もしくは同一ホスト上のクライアントを指しています。
比較して、「低速なクライアント」とは、スマートフォンなど低速なネットワークを利用しているクライアントのことです。

Unicornアーキテクチャ

Unicornのmasterプロセスはリクエストを処理する為、設定に応じた数のworkerをforkします。
1つのworkerプロセスは、同時に複数リクエストを処理する事はできません。
そのため、低速なクライアントからのリクエストは、プロセスを長時間占有してしまうという問題が発生します。

Unicorn(Rackサーバ)+Nginx(リバースプロキシ)の構成が多用されている理由

先ほど書いた様に、低速なクライアントからのリクエストを単体で処理するのには不向きなアーキテクチャですが、Nginxをリバースプロキシとして用いる事でこの問題を解決できます。

具体的には、レスポンスデータが出来上がったら、クライアントへのデータ送信を(Unicornではなく)リバースプロキシから行います。
リバースプロキシにレスポンス送信を委譲する事によって、workerは別のリクエスト処理に移る事ができます。

これが、UnicornをRackサーバとして使用する際に、Nginxなどのリバースプロキシを併用する理由の一つだと思われます。このあたりについては、WEB+DB PRESS Vol.83|技術評論社が詳しいです。


【参考】
Everything You Need to Know About Unicorn
https://blog.engineyard.com/2010/everything-you-need-to-know-about-unicorn

サービス特性に合ったRackサーバを選ぼう
http://gihyo.jp/magazine/wdpress/archive/2014/vol83

「納品をなくせばうまくいく」を読みました

「納品」をなくせばうまくいく ソフトウェア業界の“常識

※旧ブログからの転載記事です。

購入のきっかけ
IT業界の課題、開発トレンドやエンジニアの働き方に対する問題意識から「納品のない受託開発」というビジネスモデルを提唱された倉貫さんの著書です。

受託開発専業のSIerから転身して、ここ2年ほど、Webサービス/モバイルアプリをコア事業とするチームで勤務しております。
倉貫氏は受託開発、私は自社サービス開発と、ややドメインは異なりますが、「開発、リリースして終わりではなく、リリース後も改善が必要となる」という意味では、参考にできる部分も多いのではないかと思い、購入しました。

メモ

覚えておきたい部分をいくつかメモしておきたいと思います。

・扱う技術を統一化する

例えば、WebアプリケーションのフレームワークRails、サーバはAWS上に構築する、と決定してしまいます。こうする事で、ノウハウの共有や社内での助け合いも容易になりそうです。

個々のエンジニアにとって考えてみると、「学習コストを下げる事により、より深いレベルに、より早く到達できる」為、メリットの大きい方法ではないでしょうか。なお、この視点は航空業界のLCCを参考にしたものだそうです。

・幅広いスキルを備えたエンジニアが要件定義から開発・運用まで兼務する

ソニックガーデンのエンジニアは顧客との対話、設計、プログラミング、サーバ運用まで全てをこなします。これにより伝言ゲームをなくし、無駄を最小限にします。オフショアやコストメリットの大きい新興国のエンジニアに対抗するヒントになる考え方だと思います。

今後、エンジニアとして生き残るには、ワンストップで要件定義から運用までこなせる、1人でやる事でコミュニケーションコストを最小化できる事がポイントになりそうです。

・自動化と合理化

機械に出来る事は機械にさせましょう、という基本を徹底されているようです。弊社でもまだまだ人力に頼る部分が多くあり、これからも業務改善を進めて行こうと気持ちを新たにしました。

ただ、「自動化による開発/運用工数の削減」というのはその効果が見えにくい場合もあり、どうしても「機能追加、新規プロダクトの開発」が優先されやすい傾向にあります。「業務改善のための工数」をどうやって確保するか、これは現在の僕にとっては非常に難しい問題です。このあたりは近道はなく、対話を繰り返す事、個々の取り組みによるビジネスインパクトを可視化していく事が重要だと考えています。

・「属人性の排除」より、「人を大事にする」

個人的に最も感銘を受けたのが、この部分です。1人のエンジニアが担当する領域が増えるほど、実は1人のエンジニアに対する依存度は高くなります。この点に関しては、細かいノウハウはあるにしても、マネジメントや技術的な対策よりも、長く一緒に働ける関係性を維持する方が健全だと述べられています。

エンジニアの退職リスクに対する対応って、小手先の方法論で語られる事が多くて、「そもそも人が簡単に辞めないようにしよう」という方向性で語られる事って少なかったように思います。こういう姿勢は大事ですね。。。

感想
このビジネスモデルを提唱するに至った背景が、ご自身の言葉で丁寧に描かれています。
倉貫さん自身がエンジニアでもあり、共感できる部分が多くありました。

【5分で学べる】Vagrant上にRailsをAnsibleでかんたんクッキング(CentOS6, MySQL, Rails4, Unicorn)

概要

VagrantとAnsibleでRailsの開発環境を構築したら便利すぎて鼻血吹きました。 ソースコードを公開しておりますので、ご自由にお使い下さい。

https://github.com/tjnet/vagrant_sakuravps_rails

最低限のシンプルな構成になっており、把握/カスタマイズしやすいと思います。 今後は、Production環境として用いるVPS(さくらVPS)の環境構築やデプロイも実装して自動化したいと考えています。

想定している読者様

・サーバ構築の自動化に取り組みたい小規模チームの開発者
・AnsibleとかVagrantとか使ったことないけど「5分で習得したい」人
・シンプルなVagrant+CentOS+Railsの開発環境を構築して、VPS/AWS上でも動かしたい人

※開発環境(vagrant)用のものであり、nginx, capistrano,production環境(さくらVPS)用のplaybookは未完成です。今後、実装予定です。 ※playbook_vagrant.ymlをコピーして少しカスタマイズすれば、さくらVPSAWSに転用できるはずです。

経緯

とある事情から、プロダクション環境をHerokuからVPSに移す事になりました。 その為の構築手順をブログやWikiにドキュメント化し、それをコピー&ペーストするのが前時代的でダルくなってきました。 ここでは手作業で行っていた内容をAnsibleで「InfraStructure as Code」にしていく過程をご紹介します。

なぜAnsibleを使うのか?

AnsibleではChefと異なり、構築サーバ側に何かをインストールする必要は、ほぼありません。 また、僕自身はChefやPuppetを使用した事はありませんが、動作がシンプルゆえに学習コストが低いと言われています。現時点でのオフィシャルドキュメントは十分に充実しており、シンプルな構成や小規模な構成での運用事例はググればすぐに見つかると思います。

検証環境

OS X 10.9上にてコマンドを実行し、Vagrant上で開発サーバを構築しています。

Virtual Box, Vagrant, Ansibleの導入

vagrantのインストールは、ここを確認して行って下さい。

最新版Ansibleのインストールは、次のようにHomebrewを利用します。

$ brew update
$ brew install ansible

詳細はこちらをご確認ください。

仮想マシン構築の為の準備

まずは、こんな感じで任意の場所にサブディレクトリを作って、そこに仮想サーバのひな形をcloneします。

mkdir -p VM/projects
cd VM/projects
git clone https://github.com/tjnet/vagrant_sakuravps_rails.git

ディレクトリ名が長過ぎてイケてないので、任意の名前に変更します。

mv vagrant_sakuravps_rails myapp
cd myapp

ここで、仮想マシンの設定を記述したVagrantfileを確認します。

Vagrant.configure("2") do |config|

  config.vm.define :web do |web_config|
    web_config.vm.box = "centos64"
    #web_config.vm.box_url = "http://files.vagrantup.com/precise64.box"
    web_config.vm.box_url = "http://developer.nrel.gov/downloads/vagrant-boxes/CentOS-6.5-x86_64-v20140110.box"
    web_config.vm.network :private_network, ip: "33.33.33.33"
    web_config.vm.network :forwarded_port, guest: 3000, host: 8080

    web_config.vm.hostname = "develop-centos"

    web_config.vm.provider :virtualbox do |vb|
      vb.memory = 1024
    end

    config.vm.provision :ansible do |ansible|
      ansible.playbook = "provision/playbook_vagrant.yml"
      ansible.inventory_path = "provision/dev_hosts"
      ansible.limit = 'all'
      ansible.verbose = 'vvv'
    end
  end

end

主要部分について、少し補足します。

    web_config.vm.box_url = "http://developer.nrel.gov/downloads/vagrant-boxes/CentOS-6.5-x86_64-v20140110.box"
    web_config.vm.network :private_network, ip: "33.33.33.33"
    web_config.vm.network :forwarded_port, guest: 3000, host: 8080

まず、この仮想マシンはCentOS6.5を使用します。ホストOS(Mac)のポート番号8080へのアクセスは、ゲストOS(VM上のCentOS)3000へ転送されます。

    config.vm.provision :ansible do |ansible|
      ansible.playbook = "provision/playbook_vagrant.yml"
      ansible.inventory_path = "provision/dev_hosts"
      ansible.limit = 'all'
      ansible.verbose = 'vvv'
    end

また、VagrantはAnsibleのPlaybookを用いて構成管理する機能を提供しており、provision/playbbok_vagrant.ymlを用いてサーバの構成管理を行う事がわかります。 ansible.verbose = 'vvv'は、Ansibleによるサーバ構築時に、デバッグログを詳細に出力する為の設定です。

Ansibleで構成管理するための対象ホストは、provision/dev_hostsに記載しています。 []付きでグループ名を記述することが可能です。グループを指定することで、複数のサーバを同時に構築する事が可能です。

[dev_server]
33.33.33.33

PlayBookについて

AnsibleのPlaybookは、構築するサーバの構成内容をYAML形式で記述したものです。 Chefでいうレシピにあたるものです。 見れば何となく理解できますが、インストールするパッケージや処理、設定をrolesでグルーピングして記述していくと、記述されたroles内の処理(task:)が実行されていきます。

ねっ、学習コスト低いでしょ? (^^)/

- name: setting rails to server
  hosts: dev_server
  user: vagrant
  sudo: yes
  vars:
    app_name: myapp
    environtment: vagrant
    mysql_port: 3306
    home: "/home/{{user}}"
    user: vagrant
    src_dir: '/usr/local/src'
    ruby_version: '2.1.2'

    rails:
      dir: /var/www/rails/
  roles:
    - common
    - mysql
    - ruby
    - rails
    #- nginx

各記述項目の意味は次の通りです。

項目 説明
hosts 対象のサーバグループ
user 対象サーバで実行するユーザ
sudo 対象サーバでsudoコマンドを使用して実行するか
roles 各taskを任意の名前でグルーピングしたものです。各roleの処理はroles/role_name/tasks/main.ymlに記述されています。

PlayBook:最低限のサーバ設定を行う

provision/roles/common/tasks/main.ymlで、下記の処理を行っています。
・ルートログインの禁止
・パスワード認証の禁止
・EPELを追加して、パッケージの種類を増やす
・パッケージのインストール(よくわからないパッケージはyum info package_nameで確認)

PlayBook:MySQLのインストールを行う

provision/roles/mysql/tasks/main.ymlで、下記の処理を行っています。
MySQLのインストール

PlayBook:Rubyのインストールを行う

provision/roles/ruby/tasks/main.ymlで、下記の処理を行っています。 ・Rubyのバージョン確認
Rubyソースコード入手
Rubyソースコードを解凍
Rubyソースコードコンパイル
Rubyをインストール
・gemをアップデート
・bundlerをインストール

PlayBook:Railsのインストールを行う

provision/roles/rails/tasks/main.ymlで、下記の処理を行っています。
Railsアプリケーションを作成する為のDirを作成
・/etc/resolv.confに追記(これをしないとRailsのインストールが異様に遅くなります)
※詳細はVagrant+VirtualBox(CentOS6)で「gem install rails」がすっごい遅い時の対処法とか、 Slow networking (due to IPv6?) on CentOS 6.x #1172をご確認ください。
JavaScriptのランタイムがないと、おこられるのでNode.jsを導入

いよいよ実行

ここまでPlayBookの構成について書いてみました。では実際にVagrant上でサーバ構築を行ってみましょう。

vagrant up 
vagrant provision web
vagrant upでvagrantを起動します。このコマンドで、VagrantはCentOS6のboxをダウンロードする為、初回は少し時間がかかるかもしれません。 。お茶でもすすってお待ちください^^。 ちなみに、ここで下記のエラーが出てるかもしれませんが、Railsの導入や開発作業自体には支障はありません。
Failed to mount folders in Linux guest. This is usually because
the "vboxsf" file system is not available. Please verify that
the guest additions are properly installed in the guest and
can work properly. The command attempted was:

mount -t vboxsf -o uid=`id -u vagrant`,gid=`getent group vagrant | cut -d: -f3` vagrant /vagrant
mount -t vboxsf -o uid=`id -u vagrant`,gid=`id -g vagrant` vagrant /vagrant
vagrant provision webで、ゲストOS上のサーバ構築を行います。 Node.jsの導入には、少し時間がかかるかもしれません。 このVMには、
vagrant ssh
でアクセスできます。 その後は、 ・パーミッションの調整
rails new でwebアプリのベースを作成
MySQLを起動
・必要なポートをあける
iptablesの再起動
http://localhost:8080にアクセスする

と、順次設定すると、いつものRailsのトップページが表示されます。Congratulations! 詳細はREADMEをご確認ください。

TODO(今後やりたい事)

Capistranoでのデプロイ
Unicornの導入
・Nginxの導入
・プロダクション環境(さくらVPS)用のPlaybookを用意する
・厳密な冪等性を保つにはどうしたら良いのか、勉強する
・DockerとかImmutable Infrastractureに触れてみる