maenok

maeno

Front End Web Developer / WordPresser / Web Design / Infrastructure engineer / PHP / Ruby / JavaScript / CSS / html / AWS / Capistrano / git / markdown ...
  • マークダウン記法のおさらい

    DEVLAB改め、IsaBとして本格的に公式ブログ化して、今後記事書く人が増えてくるはずなので、本ブログ用にMarkdown記法の雛形、かつ表示スタイルの確認用のウェルカムドキュメント的にこの投稿を書き起こしたのですが、一般公開しても特に問題ないTIPSでもあるので、そのまま記事として公開しておきます。

    そもそもマークダウン記法って何?──という人も多いかもしれないので、

    Markdown(マークダウン)は、文書を記述するための軽量マークアップ言語のひとつである。もとはプレーンテキスト形式で手軽に書いた文書からHTMLを生成するために開発された。現在ではHTMLのほかパワーポイント形式やLATEX形式のファイルへ変換するソフトウェア(コンバータ)も開発されている。各コンバータの開発者によって多様な拡張が施されるため、各種の方言が存在する。

    上記はWikipediaからの引用です。

    まぁ、簡単に言うと、HTML知らなくてもルールに沿った書式でテキスト文章を書くことで、リッチなスタイリングがされたHTML文書に変換できてしまう記述方法と云うものです。基礎的な書式だけでも覚えておくと色んな書式に変換できるので、様々なシーンで利用できるお得な言語スキルです。

    ──と云うわけで、さっそく基礎的な書式を紹介していきます。

    1. 見出しブロック

    まずは見出しブロックの書き方から。 原則的にブロック要素の場合は、前後に改行が必要になります。

    # 見出しH1
    
    ## 見出しH2
    
    ### 見出しH3
    
    #### 見出しH4
    
    ##### 見出しH5
    
    ###### 見出しH6
    

    これがどのように見えるかというと、

    見出しH1

    見出しH2

    見出しH3

    見出しH4

    見出しH5
    見出しH6

    ↑こうなります。

    H1、H2のみ、別の記法があって、

    見出しH1
    ===============
    
    見出しH2
    ---------------
    

    ↑こんなのも使えます。 見出し直後の行の=や-の数は1個以上であればOKです。 これを利用すると、markdownがスタイリングされていないテキスト状態でも文章が見やすくなります。

    あと見出しにハッシュを使い、ページ内リンクを実現することができます(Markdown Extraの拡張機能です。このサイトでは利用可)。 その場合、見出しの後に {#hash} を付けます。

    ### ハッシュ付見出し {#hash}
    

    ハッシュ付見出し

    ハッシュ付見出しへのページ内リンクの記法は、後述のリンクの項目を参照してください。

    2. 引用ブロック

    ブロック要素なので、前後に改行が必要です。

    > 引用文のテキストが入ります。
    

    引用文のテキストが入ります。

    ...
  • GulpでCSS/JavaScriptコンパイル環境を構築する ─ CentOS編

    従来、WEBフロントエンドの主要アセットであるCSSとJavaScriptはソースレベルでなかなか管理しづらく、すぐにカオスな領域に突入して保守しづらくなっていました。そのカオスな領域にあるCSSやJavaScriptをソースレベルでもっと保守管理しやすいように生み出されたのがGruntやGulpというコンパイルビルドの仕組みです。今の世の流れ的に、フロントエンド開発ではSCSS(SASS、LESS等)、CoffeeScriptなどでCSSやJavaScriptのコーディングを効率化しつつ、同時にソースの保守管理をし易くするという開発手法がデフォルトになってきました。 実際にLESSで変数を使ったCSSスタイリングや、コード量が劇的に少ないCoffeeScriptでJavaScriptを書いてみると、圧倒的なコード生産性の高さに、もはや今までのベタなフロントエンドコーディング手法は改めざるを得ないという境地になります。 ──と云うことで、早速CSS/JavaScriptのコンパイルビルドを行う環境をサービス提供用のCentOSサーバに作ってみようかと思います(ローカル開発環境としてのWindowsマシンへの導入はこちらの記事を参照)。

    ちなみに、Gulpは「ガルプ」と読みます。私は最初「グループ」とか読んでました…(笑)

    Node.js のインストール

    まずはGulpはnode.jsのモジュールなので、node.jsをインストールしないことには始まりません。そんな訳で、node.jsをインストールするのですが、まずはnode.jsやライブラリのバージョン管理モジュールであるnvmからインストールしていきます。 下記のように、nvmをGitHubからクローンして同梱のシェルでインストールします。

    $ git clone git://github.com/creationix/nvm.git ~/.nvm
    $ . ~/.nvm/nvm.sh
    

    次に、node.jsをインストールするのですが、その前に現在のnode.jsの安定バージョンを Node.jsの公式サイト を確認しましょう。TOPページの真ん中あたりに「Current Version: v0.12.2」というように記載されているので、そのバージョンをインストールするのが無難です(2015年4月7日時点のNode.jsの安定バージョンは0.12.2でした)。 インストールバージョンが決定したら、nvmでインストールします。

    $ nvm install v0.12.2
    $ node -v
    v0.12.2
    

    バージョン確認してインストールバージョンが表示されればOKです。 最後に、自分のコンソール環境でnode.jsが利用できるように .bashrc に以下の記述を追記しておきます。

    $ vim .bashrc
    $ cat .bashrc
    # .bashrc
    
    # Source global definitions
    if [ -f /etc/bashrc ]; then
            . /etc/bashrc
    fi
    
    # User specific aliases and functions
    
    . ~/.nvm/nvm.sh
    nvm use v0.12.2
    export NODE_PATH=${NVM_PATH}_modules
    

    ついでにバンドルでインストールされたnpmのバージョンも確認してみます。

    ...
  • GulpでCSS/JavaScriptコンパイル環境を構築する ─ Windows編

    いまさら感が強いんですが、世の流れ的に、フロントエンド開発ではSCSS(SASS、LESS等)、CoffeeScriptなどでCSSやJavaScriptのコーディングを効率化しつつ、同時にソースの保守管理をし易くするという開発手法がデフォルトになってきました。私はその辺の技術の取り込みがおっくうで、ついついCSSやJavaScriptを素でコーディングしてしまったりしていたのですが、最近やっている Ruby on Rails の開発でCoffeeScriptでJavaScript書いてみて、圧倒的なコード生産性の高さに、もはや今までのベタなフロントエンドコーディング手法は改めざるを得ないという境地に達しました(いやはや、ようやくですねぇ…)。 ──と云うことで、早速CSS/JavaScriptのコンパイルビルドを行う環境を業務マシンであるWindowsマシンに作ってみようかと思います(WEBサービス側のCentOS(Linux)環境への導入はこちらの記事を参照)。

    なお、なぜビルドツールをGruntではなくGulpにしたかというと、今どきのトレンドはGulpの方かなぁ…という漠然な理由だったりするんですが、まぁ、Gruntより設定ファイルが記述しやすく、複数リソースの設定があっても設定が煩雑化しないというメリットもあるからです。

    Node.js のインストール

    まずはGulpはnode.jsのモジュールなので、node.jsをインストールしないことには始まりません。そんな訳で、node.jsをインストールします。 インストールは簡単で、Node.jsの公式サイトからインストーラーをダウンロードするだけです。私の業務用マシンは「Windows7(64bit版)」だったので、node-v0.12.0-x64.msi というファイルが対象になります(2015年3月4日時点のNode.jsのバージョンは0.12.0です)。 インストーラがダウンロードできたら、早速起動させます。いくつか対話が発生しますが、特に注意しないといけないようなインストール設定はありません。 イントールが完了したら、コマンドプロンプト(もしくは、パワーシェル)から、Node.jsのバージョンを確認してみます。

    > node -v
    v0.12.0
    

    ついでにバンドルでインストールされているnpmのバージョンも確認してみます。

    > npm -v
    2.5.1
    

    これでNode.jsのインストールは完了です。

    Gulp のインストール

    次にGulpをグローバルにインストールします。

    > npm install -g gulp
    > glup -v
    [**:**:**] CLI version 3.8.11
    

    これでインストール完了です。もしインストールに失敗するようなら、

    > npm install -g gulp --mscs_version=2012
    

    ──とオプションを付けてみると上手くいくかもしれません。 次に、ローカルにインストールします。 ローカルにインストールするにあたっては、npmの初期化を行う必要があります。まず任意にgulpを実行したいディレクトリを作成します。この記事では、C:\npm\node_modules\gulp というディレクトリを新たに作成しています。

    > mkdir -p C:\npm\node_modules\gulp
    > cd C:\npm\node_modules\gulp
    > npm init
    This utility will walk you through creating a package.json file.
    It only covers the most common items, and tries to guess sane defaults
    
    See `npm help json` for definitive documentation on these fields
    and exactly what they do.
    
    Use `npm install --save` afterwards to install a package and
    save it as a dependency in the package.json file.
    
    Press ^C at any time to quit.
    name: (gulp) gulp_build
    version: (1.0.0)
    description: Gulp-build-test
    entry point: (index.js)
    test command:
    git repository:
    keywords: gulp
    author: maeno
    license: (ISC) MIT
    About to write to C:\npm\node_modules\gulp\package.json:
    
    {
    "name": "gulp_build",
    "version": "1.0.0",
    "description": "Gulp-build-test",
    "main": "index.js",
    "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
    },
    "keywords": [
    "gulp"
    ],
    "author": "maeno",
    "license": "MIT"
    }
    
    Is this ok? (yes) yes
    

    対話式にいくつか属性値を聞かれるので、任意に入力します(すべてEnterでも可です)。 初期化ができたら、早速ローカルにgulpをインストールします。

    ...
  • Railsで何をやっても Routing Error uninitialized constant ~と言われて泣きそうになった時

    昨日まで、午前中まで、ちょっとさっきまで、正常に動いていた Ruby On Rails のアプリが、急に何をやっても「Routing Error : Uninitialized constant XXXXX Controller」というエラーになってしまい、うんともすんとも言わなくなってしまうという謎の症状に陥り、原因究明まで1ヶ月ほど泣きそうになっていたというしびれる経験をしたので、ここに解決までのTIPS(もとい、奮闘記?)を書いておこうかと。

    犯人捜索編

    まずはもうトラウマに近くなったこのRailsのエラー画面…。

    Routing Error

    まず、このエラーに陥ると、Railsくんは直接URLを指定して呼び出されるコントローラーを切り替えても、いやもうそれ以外何をしても、ただひたすら同じ「Routing Error」でコントローラ名だけ違うというエラーを吐き続けます。もうそれしかエラーメッセージ知らないかのようにそれしか言わなくなってしまうんです。でも、Railsのルート設定ファイルである config/routes.rb は正常なのです(だってさっきまで動いてたし、ルーティング変えてないんだから当然ですよ)。Railsのログ見てもエラー画面と同じ程度の情報しか出ないし、サーバ側のエラーログ見ても毎回 404エラー で、そのルート(URL)にはコンテンツがない(Railsのcontrollerが動いてない)よ──としか書いてない。エラーログの処理スタックを遡って行っても特におかしいところがない… いったいこりゃなんだ!? もう心は折れました、だいぶベキベキと折れて、Rails嫌いになりました、でも捨てられません、お仕事なので、原因究明させないと前に進めないんです(T_T)

    StackOverflowとかサポートフォーラムを検索すると、同じ症状に陥っている人の質問がいくつかあるんですよねぇ…でも、回答が書いてない。 まじかよ~誰も解決してないのかよ~!? 誰か助けてくれ~…という状態。

    そしてこの症状のさらに謎なところが、Rails環境を再構築(ゼロからすべてインストールし直す)と解消するのです(というか、その当時はそれでしか復旧する術がなかったのですがね)。そのため、最初はサーバ側(ApacheやRailsを中継していたPassengerとか)の設定とかキャッシュとかが問題なのかとか色々調べたんですが、すべて的外れでした(バグフィックスの時に「ここじゃね?」という勘が外れると凹むんだよねぇ…自分も老いたな──とか思ってしまう)。

    さて、何度目かのRails再ビルドで、RubyのバージョンとRailsのバージョンを変えてみました。最初はRuby1.9.3+Rails4.1.0だったのを、Ruby2.0.0+Rails4.2.0にしてみました。バージョンってのには何気に相性がかなりあるので、その辺から攻めてみた次第。リビルド直後は問題なし、でも次の日のお昼にまた同じ症状が発生してしまった。 RailsのSQLiteデータベースが壊れているのかもと、リビルド直後にバックアップしていたDBに切り戻してみても駄目だ。もう一度DB初期化してデータを入れ直してみる。…ん? seeds.rb が通らない(Railsではデータベースに初期データを登録する時は db/seeds.rb を使って rake db:seed コマンドを実行するのだが、これがエラーになるのだ )。これは Rake のバイナリが壊れているのか?

    ──いやぁ、このあたりが一番泣きそうな感じでしたねぇ…もう徒労感と絶望感しかない感じ。閃きも出ないしね。

    そんなこんなで、一ヶ月近く同じ調査を続けてたのですが、そこにようやく光明が差し込んだ次第。いやぁ、苔の一念…ってヤツですかね~。なんと、こんなサイトを発見!

    Rubyが突然動かなくなった “/usr/bin/ruby: No such file or directory”

    prelink とな? CentOS環境では、必須でインストールされており cron.daily (一日一回動くcronジョブ) で勝手に実行されちゃう。弊害としてRubyのバイナリファイルを壊してしまう場合がある──とのこと。かなり怪しい。今度は prelink+ruby で検索してみるとようやっと出てきました…金脈にたどり着いた感じだ。

    ...
  • DBのプライマリキーは複合主キーではなく、サロゲートキーを使うべし

    別件でUnicodeの「サロゲートペア」について調べていたところ、データベースの「サロゲートキー」と「複合主キー」についての記事が サロゲート つながりで引っかかって来て思いっきり脱線しました(笑)

    そんなこんなで、あまりデータベースに慣れ親しんでいないと、「サロゲートキー」「複合主キー」「ナチュラルキー」って何?──となる人は多いはずなので、自分自身のおさらいも兼ねて一度まとめておこうかと思った次第。

    プライマリキー(主キー)

    データベースのたいていのテーブルに存在する、データを一意に管理するためのキー制約。このキーが指定された列中には重複する値を入れることができず(ユニークキー制約)、空白(NULL)にすることもできない。

    ユニークキー(一意キー、ユニークインデックス)

    このキーが指定されている列中には重複する値を格納できず、一意にならなければならないというキー制約。ただし、プライマリキーと異なり空白(NULL)は格納可能で、空白(NULL)のみは重複ができる。このキーが指定されると同時にインデックスも貼られるため、ユニークインデックスと同義である。

    サロゲートキー(代理キー)

    オートインクリメント属性等により、連番の数値が格納されているユニークな値を持つカラムに対してプライマリキー制約を付与した場合のプライマリキーのこと。テーブル内で一意となる唯一の行レコードを特定するための列の集合(ナチュラルキー)を代替するため代理キーとも言われる。単一の列で全行レコードの一意性を識別できるのでシステム的に有益なのだが、実サービス等に利用するデータとしては意味をなさず、データ利用者側の観点からだとデータベースの物理ストレージ領域を無駄に圧迫する列でしかない。

    ナチュラルキー(自然キー)

    テーブル内の行レコードを特定するためのユニークキー制約のついた列の集合のこと。行レコードの一意性が識別できれば単一の列でもかまわないが、例えば、WordPressだと「投稿ID」「タグID」の組み合わせで、とあるタグに属する投稿データを一意に識別できるような場合、それら2列の集合体がナチュラルキーと呼ばれ、そのナチュラルキーを持つテーブルがwp_term_relationshipsだ。原則的にナチュラルキーに含まれる列はユニークキー制約をもっていなければならないため、プライマリキーに設定されていることが多い。データ利用者側の視点では、意味あるデータのみによって一意性が識別されるナチュラルキー型テーブルは用途が明確で無駄がない(と見えるようだ)。

    複合主キー

    複数プライマリキー、サロゲートキー+ナチュラルキー、プライマリキーなしの複数列集合のナチュラルキーのみといったテーブルが複合主キー型のテーブルといえる。要は単一列のみで行レコードの一意性が識別できないタイプのテーブルである。他のテーブル同士をリレーションさせるために外部キーの結合のみを管理するようなリレーションテーブルによく見られるタイプ(ナチュラルキーの項で紹介した、WordPressのwp_term_relationshipsテーブルなど)。リレーションテーブルを覗いたことがある人やDB設計したことがある人には良く理解できるはずだが、このような複合主キー型テーブルはそれぞれの外部テーブルのサロゲートキーを集中管理するような構造になっていて、実データのリレーション関係が一目ではわからないのが常だ。かといって、ナチュラルキーをプライマリキーとしたテーブルのプライマリキーを外部キーとしてリレーションさせるとそのリレーションテーブルを見るだけでデータの結合性が一目でわかるものの、参照コストの高い文字列データ等がデータベース内に重複して存在することになって、データベース全体としてパフォーマンス低下を招く上、物理ストレージ領域の圧迫もサロゲートキー以上に進んでしまう。 また、特にナチュラルキーが一意性識別に関与するようなテーブルだと、ユニークなデータを取り扱う際のシステムコストが高くなる。

    言葉の整理をしてて改めて感じたのだが、複合主キーのテーブルを操作するのは結構面倒そうだということ。まぁ、実際に面倒なんですがね。例えば、複合主キーのナチュラルキーのうち一つの列の値を変更したい場合、その行レコードを特定するために同じナチュラルキーを検索条件に入れなければならなかったりして、ちょっと処理を考えるのがイヤになる。 プライマリキーが二つ以上あるテーブルをUPDATEしようとするとMySQLに怒られたりするしね…。

    私個人の結論としては、データベースは人ではなく、システムが応答し易いように作った方が良く、パフォーマンスを犠牲にしてまで人間側のデータ視認性にこだわるのは本末転倒だと思いますね。人間側に認識し易いデータは別途中継処理等を作って整形してあげるのがデータベースを利用するシステムの正しいカタチではないかと。なぜなら、人間側が要求するデータは、時と場合と人によってフィルタリングさせないと有益なデータにならないケースがほとんどだから…。つまり、プライマリキーは、複合主キーとかは使わず、サロゲートキーを使うのがベストプラクティスだと思うわけです。

    ...
  • Rubyのややこしい配列とハッシュとシンボルについて整理してみた

    皆様、あけましておめでとうございます。

    さて、2015年の初投稿は、私的に今最も熱いRubyネタで始めようかと思います。

    まず基礎のおさらいとして、Rubyにおける配列には一次元配列のarrayクラスオブジェクトと多次元配列(連想配列)であるhashクラスオブジェクトの二種類があります。それぞれの配列オブジェクトは定義する際に要素を囲い込むリテラル記号によって以下のように区別されています。

    array = ["A", "B", "C"] # 配列変数arrayを定義
    hash1 = {:first => "A", :second => "B", :third => "C"} # ハッシュhash1を定義
    

    一次元配列であるarrayクラスオブジェクトは各要素値に数値添字が自動で割り振られるため、数値添字のインデックスにて各要素にアクセスができます。

    array = ["A", "B", "C"]
    puts array[0] #  "A"が表示される
    

    一方hashクラスオブジェクトはキー・バリュー型のオブジェクトなので、文字列型の添字(キー)で要素値にアクセスができます。

    hash2 = {"first" => "A", "second" => "B", "third" => "C"}
    puts hash2["first"] # "A"が表示される
    

    さて、ここで一番最初の例で定義したhash1とhash2では、キーの指定の仕方が異なっていることに気づきましたでしょうか? hash1とhash2は要素値こそ一緒ですが、キーが異なるため同じハッシュではありません。

    puts hash1 == hash2 # falseになります
    

    hash1は ハッシュキーがシンボルになっているハッシュで、hash2は文字列をキーとするハッシュであるため、それぞれ異なるハッシュとなります。なお、ハッシュのキーはシンボルであろうが文字列であろうがハッシュ内でユニークである必要があります。また、文字列キーとシンボルを同名で混在させた場合、それぞれの要素が別のものとして扱われます。

    ...
  • AWS SDK for Rubyを使ってEC2インスタンスのステータスを確認する

    アプリケーション側で、任意のAWSのEC2インスタンスについて、現在の稼働状況をリアルタイムに確認したい時がある。例えば外部アプリケーションからEC2インスタンスを起動させたり、停止させたりする場合などに、インスタンスの稼動状況を確認してステータスが変わったら次のプロセスを実行したいとかいうケースが、それに当てはまる。

    SDK for Rubyでは、AWS::EC2クラスのclient.describe_instance_statusメソッドで特定のECインスタンスのステータスを取得することができる。インスタンスのステータスにはsystem_statusinstance_statusの2種類があって、正確な稼動状態を確認したい場合はこの2つのステータスを取得する必要がある。 そして、注意しないといけないのが、インスタンスが停止している時はステータスが存在していないということだ。つまり、停止しているインスタンスに対してclient.describe_instance_statusメソッドを実行した場合、ステータス情報が格納されているinstance_status_setオブジェクトの中身が取得できないので、それを踏まえてコーディングしないと、ステータス取得エラーとなってしまう。 例えば、停止状態のインスタンスを起動した場合のステータスを監視する時など、最初はインスタンスが停止しているので、instance_status_setオブジェクト内にステータスがないということをあらかじめ想定して処理を作る必要がある。 また、ターミネートされたインスタンスはclient.describe_instance_statusメソッドを実行する対象自体がないことので、こちらも注意する必要がある。

    さて、上記もろもろを踏まえてEC2インスタンスのステータス確認するRubyプログラムを作ってみた。

    chkins.rb
    # encoding: utf-8
    require "aws-sdk"
    
    def check_instance_status
    
        ec2 = AWS::EC2.new(
          :access_key_id => Params[:access_key_id], 
          :secret_access_key => Params[:secret_access_key], 
          :region => Params[:region]
        )
    
        AWS.memoize do
          status = []
          if ec2.instances[Params[:instance_id]].exists?
            ec2info = ec2.client.describe_instance_status({ 'instance_ids' => [Params[:instance_id]] })
            if ec2info.instance_status_set.empty?
              # instance has stopped
              status << 'stopped'
              message = '%s has stopped'
            else
              ec2info.instance_status_set.map { |i| 
                sys_status = i.respond_to?(:system_status) ? i.system_status.details[0].status : nil
                ins_status = i.respond_to?(:instance_status) ? i.instance_status.details[0].status : nil
                status << sys_status << ins_status
              }
              if status.include?('passed')
                # instance is running
                message = '%s is running'
    
              elsif status.include?('initializing')
                # instance is initializing
                message = '%s is starting'
    
              else
                # otherwise
                message = '%s is unknown status'
    
              end
            end
          else
            # instance has terminated
            status << 'terminated'
            message = '%s has already terminated'
          end
          puts sprintf("#{message}. status: %s", "Instance: #{Params[:instance_id]}", status)
        end
    end
    
    begin
      # init
      Params = { 
        :access_key_id => ARGV[0], 
        :secret_access_key => ARGV[1], 
        :region => ARGV[2], 
        :instance_id => ARGV[3], 
        :timeout => ARGV[4].nil? ? 1 : ARGV[4].to_i
      }
      raise 'Parameter is missing.' if Params.values.include?(nil)
    
      for i in 1..Params[:timeout] do
        sleep 1 if i > 1
        check_instance_status
      end
    
    rescue => e
      puts e
    end
    

    プログラム実行時の引数として、

    ...
  • Capistrano3のタスク内でトップレベルのタスク名を取得する

    Capistrano3でタスク書いていて、タスク内でcapコマンドとして実行されたトップレベルのタスク名を引いて、処理を分岐させる必要が生じて、そのトップレベルのタスク名取得に苦労したので、ここに備忘録化しておく。

    基本的にCapistrano3でタスク内で自分のタスク名を取得したい場合、次のようにブロックの引数として引ける。

    desc "main task"
    task :main_task => :sub_task do |task|
        run_locally do
            info ": This task name: #{task}"
            info ": Running main deploy task!"
        end
    end
    
    desc "sub task"
    task :sub_task do |task|
        run_locally do
            current_task = task.name_with_args.split(':').last
            info ": This task name: #{current_task}"
        end
    end
    

    上記タスクを実行してみると、こうなる。

    $ cap test deploy:main_task
    INFO: This task name: sub_task
    INFO: This task name: deploy:main_task
    INFO: Running main deploy task!
    

    ただ、デプロイの共通設定を読み込むとかのサブタスクを作って、全てのメインタスクの前にそのサブタスクを実行するようなタスクチェーンを構築した場合に、特定のメインタスクが実行された時のみ、サブタスクの一部処理を分岐させたいとかの要望が発生すると、サブタスク内でメインタスクのタスク名を取得する必要が出てくる。つまりは、capコマンドで実行されるトップレベルタスク(例で言うところのメインタスク)を取得したいのだが、これがなかなかTIPSが見つからなくて実現するのに苦労した。 Capistranoのタスク処理は、コアで使われているRakeクラスで実現されているので、そのRakeクラスのApplicationメソッドから取得する必要があった。

    ...
  • 素のRubyでMySQLクエリの結果を取得する

    Ruby+MySQLの処理をする時、たいていはMySQL操作系のライブラリでruby-mysqlmysql2とかを使うケースが多いのだろうが、それらのライブラリなしの素のRuby環境でMySQLのクエリ操作を行う必要があったので、その際に書いたソースを汎用化してまとめてみた。まぁ…ほとんどニーズはないかもしれんが、一応TIPSとして共有化しておこうかと。

    mysql_query.rb

    db = { :db_config => "~/.user.cnf", :db_name => "database_name" }
    
    query = "SELECT * FROM db_table_name WHERE primary_key_id = 1;"
    res = `/usr/bin/mysql --defaults-extra-file=#{db.fetch(:db_config)} -D #{db.fetch(:db_name)} -e \"#{query}\" `
    
    if res.empty? then
        p "data is none."
    else
        lines = res.split("\n")
        columns = lines.first.split("\t")
        results = []
        lines.each_with_index { |line, i| 
            if i > 0 then
                tmp_hash = {}
                values = line.split("\t")
                values.each_index { |j| 
                    if values[j].match(/^\d+$/) then
                        tmp_hash[columns[j]] = values[j].to_i
                    else
                        tmp_hash[columns[j]] = values[j].to_s
                    end
                }
                results << tmp_hash
            end
        }
        p results
    end
    

    やってることは、コマンドラインでMySQLのクエリを発行しているのと同義で、標準出力としてのMySQLクエリの結果をRuby側で取り込んでハッシュにパースしているだけです。 なお、MySQL5.6からコマンドラインにパスワードを付与してmysqlコマンドを実行すると警告が出てしまうので、MySQLへの接続は.user.cnfのファイルに書いて、そっちからインポートするようにしている。

    ...
  • authorクエリを利用したWordPressのユーザー名漏洩を防ぐ方法

    DEVLABはWordPressで運用しているのだが、そのWordPressについて気になる記事を見つけた。 WordPressサイトのホームURLに「?author=x」のGETクエリをつけることで、サイト内のユーザー名がばれてしまう というものだ(※ 詳しくはMT SystemsさんのWordPress TIPを参照)。?author=xのxは数値で、WordPressのユーザーID(ユーザーテーブルのプライマリキー)となる。つまりxに1を指定すると、WordPressのルート管理ユーザーになるわけだ。 さっそく、DEVLABでも試してみたところ・・・ https://dev.blog.colorkrew.com?author=1がみごとにパーマリンク設定で指定されているリライトルールに沿ってリライトされ、https://blog.colorkrew.com/author/<ルート管理ユーザー名>/にリダイレクトされてしまった。このままだと、管理ユーザーのユーザー名に対してパスワードの総当りをされて、もしログイン成功とかされちゃうと、サイトがクラッキングされてしまうじゃないですか! ただ、DEVLABの場合、一般的なWordPressのログイン画面wp-login.phpは無効化してあって、ログイン方法を探すのが大変なので、一応は安全ではある。しかし、ユーザー名が漏洩してしまう脆弱性があるのは防止しないといけないので、対策を考えてみた次第。

    MT Systemsさんのサイトで紹介されているように、著作者アーカイブページのテンプレートauthor.phpによってリダイレクトしてしまう方法ではHTTPのレスポンスヘッダにリダイレクトのlocationとしてユーザー名が出力されてしまうため、たとえ.htaccessにrewriteルールを追加したとしても防止できないのが厄介だ。 MT Systemsさんのサイトでは、最終的にユーザーデータのuser_nicenameを書き換えて、ログインアカウントであるuser_loginと異なる値にしてしまう対処策が掲載されていたんだが、管理パネルから直接編集できない項目でもあって、なかなかに難儀である。

    もうちょっと簡単に対策できないものか…。 ──と、言うわけで、対策方法を自作してみた。

    // prevent the leakage of user name by author query on WordPress.
    function knockout_author_query() {
        // disable author rewrite rule
        global $wp_rewrite;
        $wp_rewrite->flush_rules();
        $wp_rewrite->author_base = '';
        $wp_rewrite->author_structure = '/';
        // for author query request
        if (isset($_REQUEST['author']) && !empty($_REQUEST['author'])) {
            $user_info = get_userdata(intval($_REQUEST['author']));
            if ($user_info && array_key_exists('administrator', $user_info->caps) && in_array('administrator', $user_info->roles)) {
                wp_redirect(home_url());
                exit;
            } else {
                // enable author rewrite rule
                $wp_rewrite->author_base = 'author';
                $wp_rewrite->author_structure = '/author/%author%/';
            }
        }
    }
    add_action('init', 'knockout_author_query');
    

    上記のソースをテーマのfunction.phpなどに追加することで即時有効になります。

    ...
  • Capistrano3でタスクが二重起動してしまう時の対処法

    Capistrano3でステージ環境を変えてタスクを実行した時に、タスクが二重起動するという症状に陥った。二重でタスクが実行されるので、ラウンチするAWSインスタンスを“2つ”と設定していても“4つ”起つし、MySQLに発行するクエリも2倍になって、INSERTするレコードが重複して挿入される。一時的にファイルに書き出していた設定なども二重起動している後続タスクによって上書きされてしまい、もうデプロイはしっちゃかめっちゃかな状態だった。 解決できたので、その時の対処法を備忘録として残しておく。

    原因はinvoke設定でデフォルトのデプロイ環境を指定していたためだった。

    はじめ、試験環境でデプロイタスクの開発を行っていた時、Capfileに下記のような設定を書いていた。

    Rake::Task[:develop].invoke
    invoke :develop
    

    これを書いておくと、デフォルトデプロイ環境がdevelopに固定化されるので、デプロイコマンドを実行するときに本来ならデプロイ環境(ステージ名)を指定して、

    $ cap develop deploy:task_name
    

    のようにするところを、

    $ cap deploy:task_name
    

    と、デプロイ環境を省略できてちょっとだけ楽だったのだが、これがデフォルトデプロイ環境以外にデプロイを行う時に悪影響を及ぼしたのだ。

    今回、試験環境でデプロイが上手くいったので、次に本番環境でデプロイを行うことになり、デプロイ環境(ステージ名)をproductionで実行することになった。デプロイ内容は試験環境とまったく同一だったため、デプロイタスク設定の差分はなく、ロール設定やSSH接続設定も同じだったため、config/deploy/develop.rbをコピーしてconfig/deploy/production.rbを作成していた。 この状態で、下記のようにデプロイ環境を指定してデプロイを実行した次第。

    $ cap production deploy:task_name
    

    これだと、省略されているデフォルトデプロイ環境へのデプロイも同時に起動してしまうわけです。実際には、

    $ cap develop production deploy:task_name
    

    と環境を二つ指定してデプロイを実行しているのと同じことになっているわけです。

    この状態を解消するには、前述のinvoke設定を削除するだけです。 というか、複数環境に対してデプロイが発生する時は混乱の元になるんで、invoke設定とかはしない方がいいですね。

    いやはや、何気に解決するまで数時間ハマってました。今後は注意しないと。

    ...
  • Capistrano3でEC2インスタンス新規作成から初期設定までのデプロイ(まとめ)

    ここまでAWSのEC2インスタンスを新規作成して、そのインスタンスに対しての初期設定までを、Capistrano3でタスク化することをやって来ました。難儀したものの、ようやくEC2インスタンスの準備が出来て、あとはミドルウェアやアプリケーションをインストールするだけ──というところまでたどり着きました。そこで今回は、これまでのデプロイの流れを一度総括してまとめてみようかと思います。

    まず、Capistrano3を稼動させるデプロイサーバの準備から。公式のAmazon Linux環境などのEC2インスタンスをAWSマネージメントコンソール等からラウンチして、ログインしたら、Capistrano3をインストールします1

    # yum update -y
    # yum groupinstall -y "Development Tools"
    # openssl version
    OpenSSL 1.0.1h-fips 5 Jun 2014
    # openssl version -d
    OPENSSLDIR: "/etc/pki/tls"
    # curl -L https://get.rvm.io | bash -s stable
    # source /etc/profile.d/rvm.sh
    # rvm list
    (※ rvm rubies と表示されればインストール完了)
    # ruby -v
    ruby 2.0.0p451 (2014-02-24 revision 45167) [x86_64-linux]
    # rvm install 2.0.0 -- --with-openssl-dir=/etc/pki/tls
    # ruby -v
    ruby 2.0.0p481 (2014-05-08 revision 45883) [x86_64-linux]
    # gem install rails --no-ri --no-rdoc
    # gem install capistrano
    # gem install capistrano_colors
    # gem install capistrano-ext
    # gem install railsless-deploy
    # cap --v
     (※ cap aborted! ~と表示されればインストール完了)
    # gem install aws-sdk
    

    デプロイサーバにてデプロイプロジェクトを実行するユーザを作成しておきます。デプロイタスクの内容にもよるのですが、対象のユーザはsudo権限を持っていた方が都合が良いかと思います。ユーザを作成したら、そのユーザで再ログインして、ホームディレクトリで、Capistrano3用のプロジェクトを作成します。

    ...
  • Capistranoのタスクを新EC2インスタンスが完全起動するまでsleepさせる

    Capistranoで新規作成したEC2インスタンスが完全に起動し切っていない状態で、そのインスタンスに対してSSHアクセスするタスクを実行すると、どこかしらでエラーになってタスクが完了しません。そこで、デプロイ対象となるEC2インスタンスの起動状態をチェックして、完全に起動していない状態の場合sleepして起動を待つようなタスクを作りました。

    AWSのEC2インスタンスには3つのステータス情報があり、この全てのステータスを確認しないと、インスタンスの完全起動状態とは言えないので、注意が必要でした(下図参照)。

    AWSマネージメントコンソールで確認できるEC2インスタンスステータス

    インスタンスが起動しているかどうかの確認は、AWSマネージメントコンソールでいうところの「Instance State」がrunningであるかを判定すればOKなのですが、実際にインスタンスにSSH接続ができるかどうかの確認は「Status Checks」欄に「2/2 checks」とあるように2つのステータス(INSTANCESTATUSとSYSTEMSTATUSのreachability)が共にpassedであるかまでを確認する必要があるのです。通常、インスタンスが起動すると「Instance State」は数秒から十数秒程度でrunningになるのですが、「Status Checks」は数分程度initializingで初期化処理を行っています。このステータスが共にpassedにならないとSSHアクセスでコケます。

    今まで私が使っていたcheckタスクだと、「Instance State」のステータス1つしか確認していなかったので、後続タスクがSSHアクセスで中断したりしていました。それを回避するためのタスクが今回のcheckタスクになります。

    task :check do
      run_locally do
        created_instances_list = 'CREATED_INSTANCES'
    
        def check_instance_status(instance_ids=[]) 
          ec2 = AWS::EC2.new
          AWS.memoize do
            ec2info = ec2.client.describe_instance_status({'instance_ids' => instance_ids})
            sys_status = ec2info.instance_status_set.map { |i| i.system_status.details[0].status }
            ins_status = ec2info.instance_status_set.map { |i| i.instance_status.details[0].status }
            status = sys_status + ins_status
            return status.include?('initializing') ? false : true
          end
        end
    
        ec2 = AWS::EC2.new
          begin
            if test "[ -f ~/#{created_instances_list} ]"
              created_instances = capture("cd ~; cat #{created_instances_list}").chomp
              ci = created_instances.gsub(/(\[|\s|\])/, '').split(',')
              target_instances = ec2.instances.select { |i| i.exists? && i.status == :running && ci.include?(i.id) }.map(&:id)
              raise "Is not still created all instances" if target_instances.length < fetch(:instance_count)
              if target_instances.length == fetch(:instance_count) then
                # 全インスタンス起動(Instance Stateがrunning)
                chk_retry = 0
    
                while !check_instance_status(target_instances) do
                  # インスタンスステータスが全てOKでない場合は15秒待つ(※20回までリトライする)
                  info "In preparation of the instance: Status check " + (chk_retry > 0 ? "(retry #{chk_retry + 1} times)" : "")
                  sleep 15
                  if check_instance_status(target_instances) then
                    break
                  end
                  chk_retry += 1
                  raise "Instance is not still ready. Please run the task again after waiting for a while." if chk_retry >= 20
                end
    
                # ここからが全インスタンス完全起動後の処理(初回ssh接続設定)
                target_instance_private_ips = ec2.instances.select { |i| i.exists? && i.status == :running && ci.include?(i.id) }.map(&:private_ip_address)
                pkfn = fetch(:private_key_file)
                target_instance_private_ips.each { |var| 
                  server var, user: 'ec2-user', roles: %w{web app}, ssh_options: { keys: %W(/home/deploy-user/#{pkfn}), forward_agent: true }
                }
              end
            end
          rescue => e
            info e
            exit
          end
      end
    
      # 後続タスクのサンプル(SSHしてhostnameを表示する)
      on roles(:web) do
        info capture "hostname"
      end
    end
    

    上記設定から、後続タスクのサンプル部分を削除したタスクを、CapistranoでEC2インスタンスを作成した後のデプロイタスクの直前に挿入してやる感じです。

    ...
  • Capistranoで新規作成したEC2インスタンスの初期設定

    本項では、Capistranoで新規作成したEC2インスタンスへSSHで初回ログインした際の、保守用ユーザの作成、初期ユーザに対してのパスワード設定、サーバのホスト名設定など、いわゆるサーバ環境の初期設定を行うタスクを作ってみます。

    その前に、ホスト名のつけ方としての色々と試してみての所感なのですが、初回SSHログイン後にそれぞれのインスタンスに対してHOSTS設定する際にEC2側にタグ付けてしていってもできるのですが、まずインスタンス作成する時にあらかじめホスト名の元となるタグを付けておいて、各インスタンスログイン後はそのタグを参照してHOST名を設定する方がスマートだと思いました。特に複数インスタンスを同時に立てる時などにホスト名に連番を振りたいとかいう時は、カウンター変数を使って回しているec2.instances.create()時にそのカウンターの数値を転用できるので簡単でした。ということで、インスタンス作成時にタグを追加する方法ですが、

    # タグ情報
    set :host_name, 'deploy-client'
    
    ~(中略)~
    
        created_instances = []
        cnt = 0
        while cnt < fetch(:instance_count) do
          i = ec2.instances.create(
            ~(中略)~
          )
          sleep 10 while i.status == :panding
          i.tags['Name'] = [ fetch(:host_name), format("%02d", cnt+1) ].join('-')
          created_instances << i.id
          cnt += 1
        end
    
    ~(省略)~
    

    ── と、ec2.instances.create()の後でタグを付けてやればOKです1。 今回はこのNameタグの値をその後のタスクでインスタンスのホスト名として利用します。

    いきなり横道に逸れましたが、本題に戻ります。 初回SSH時のタスクとして、前回作成したinitタスクを使います。流れとしては、デプロイサーバ側で新たに作成するユーザ用のキーペアを作成しておいて、デフォルトユーザにてSSHログイン後、まず保守用の新規ユーザアカウントを作成ます。その後、そのユーザに公開鍵認証によるSSH設定を行い、デフォルトユーザにはパスワードを設定してsudo権限を剥奪、ホスト名を設定して一旦ログアウトしています。 まず、タスク設定前に各種パラメータを定義します。

    ...