仕事を楽しくする【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で構築手順を半自動化しています。
AWSやVPSでサービス開発を行う場合、手間がかかりそうな所といえば、デプロイ環境の構築でしょうか。手元のMacで開発=>本番のさくらVPSにデプロイ、といった構成にしたい場合は、自分でこれを設定しなければなりません。
セキュリティの設定、デプロイツールのセットアップをすっとばしたい、という場合はHerokuでしょうか。
デプロイに関しては、Herokuは圧倒的に簡単です。
手元のMacで開発したプロジェクト配下で"git push heroku master"とかやれば、デプロイ完了です。最も初速の出やすい開発プラットフォームではないでしょうか。
技術的制約
今回は、ニュースアプリの作成を予定しているため、サーバにサムネイルを一時保存したかったのですが、Heroku単体ではそれができない為、色々検証した結果、Herokuは選択肢から外しました。
次にAWS。スタートアップ界隈では実質業界標準になってきている(気がする)AWSには、正直何でも搭載されている感があります。スケールさせやすかったり、Herokuで出来ないファイル保存ができたり、、。ただ、趣味の開発でここまで必要なのか。。と言われると正直必要ない場面も多そうですね。色々有りすぎるために、把握しないといけない範囲が増えて手が止まるのは嫌ですね。
さくらのVPSは、AWSみたいにポチポチインスタンス増やしてスケールさせたりは出来ません。ただ、ピュアなLinuxであり、root権限も付与されているため、1台にNginx+(Unicorn+Rails)+MySQL全部盛り、みたいな構成にするならVPSで十分ですね。
開発環境/本番環境を構築する上での前提
というあたりを意識して作りました。個人開発なら、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のあたりは見よう見まねで、おかしい所が多々あるかもしれません)。
デプロイ設定をGitHubで公開するかどうかは、少し悩みました。ただ、ローカル(vagrant)のHostsファイルに本番環境のIPアドレスを記述するようにすれば、具体的なサーバの情報をGitHubに公開してしまう事もありません。アドバイスを頂けるチャンスもあるかもしれないですし、こんな情報でも誰かの役に立つかもしれないので、積極的にシェアしていきたいと考えています。
かかるコスト
かかるコストですが、シンプルに使ったサーバ台数x使用したVPSの月額料金です。
【怠惰な】Swiftを学ぶにあたって最適なオープンソース【Objective-C経験者向け】
筆者のスペック
業務やプライベートプロジェクトでObjective-C を1年半ほど、経験しました。最近、サンプルコードやライブラリもSwiftを用いたものが増えて来て、少し焦っています。Swift勉強しなくちゃ。
困っていること
学習に適したアプリ
とりあえず、いくつか試してみて、筆者の環境(Xcode6.1)で動作確認できたソースはこちら。
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の勉強中に作成したサービスだそうです。
アーティストの名前を入れると、楽曲リストを順番に再生してくれます。
素晴らしいです。勉強させて頂きます。
参考資料
正直、全然わからない。。。この辺りを参考にしながら進めていきます。
初心者向け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&count=horizontal&id=twitter-widget-0&lang=en&original_referer=http%3A%2F%2Fyusukebe.github.io%2FFMTube%2Findex.html&size=m&text=FMTube!&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&width=150&layout=button_count&action=like&show_faces=true&share=true&height=21&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&rel=0&enablejsapi=1&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に登録する方法が複数あり、大分とまどいました。
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をコピーして少しカスタマイズすれば、さくらVPSやAWSに転用できるはずです。
経緯
とある事情から、プロダクション環境を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 webvagrant 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 /vagrantvagrant 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に触れてみる