atijust's blog

技術的なこととか。

Laravel + Codeception2 でブラウザテストを自動化する

この記事はLaravel Advent Calendar 2014 22日目です。今日はCodeceptionを使ったLaravelアプリケーションのブラウザテストの自動化を紹介したいと思います。

はじめに

Webアプリケーションの開発現場でよくある光景として、ブラウザを使って人力でポチポチやって動作確認をしたりしますよね。ユニットテストを書いているといっても、Webサーバやデータベースまで含めた実際に動いているシステム全体の結合テストとして、リリース前にはブラウザでの動作確認はできればやっておきたいもの。ただ、一度や二度ならまだしもリリースの度に何度も何度も人力でテストするのは大変です(主に精神的に)。リリース頻度が日に数度といったレベルの場合は、そもそも人力テスト自体が不可能になるということも。もうこれは自動化するしかないですね。ということでCodeceptionです。

Codeceptionはユニットテスト、機能テスト、受け入れテストをサポートしたフルスタックPHP用テスティングフレームワークです。いわゆるBDDフレームワークといわれるもので(厳密にいうとどうなのかはわかりませんが……)、自然言語(英語)に近い記法でテスト対象の振る舞いを記述しテストを行うことができます。

Codeceptionがサポートする3種類のテストのなかでブラウザによるテストに相当するのが受け入れテスト(Acceptance Test)です。受け入れテストとは、アプリケーションの利用者であるユーザの視点から、アプリケーションが期待したとおりに動作するか確認するテストのことです。サイトトップにアクセスして、ナビバーからログインページに飛んで、ユーザ名とパスワードを入力して、ログインボタンをクリックして、Welcome!と表示されたらOK……こういったユーザからみたアプリケーションの挙動を記述したシナリオを元に、Guzzle+Symfony BrowserKitによるスクレイパー(CodeceptionではPhpBrowserと呼ばれています)やPhantomJSSelenium WebDriverで実際に動作中のアプリケーションにアクセスして挙動をテストすることができます。

この記事ではLaravel Tricksをサンプルとして、Codeceptionで受け入れテストを記述する方法を紹介します。受け入れテストの実行にはPhpBrowserを使用します。PhpBrowserはJavaScriptの実行はできませんが、Codeceptionに標準で入っているのでお手軽です。JavaScriptが必要な場合はPhantomJSやSelenium WebDriverを使用する必要がありますが、これらは別途インストールが必要です。詳しくはCodeceptionのマニュアルを参照してください。

Laravel Tricksのセットアップ

受け入れテストを行うにはWebサーバ、データベース含めてアプリケーションが動作している必要があります。下準備としてまずはLaravel Tricksをローカル環境で動くようにしておきます。

Laravel Tricksのセットアップ自体は本題ではないので、ここではわたしの開発マシンにあわせてあらかじめ設定を済ませておいたforkを利用します*1。Laravel Tricksのセットアップについて詳しくはLaravel Tricksのドキュメントを参照ください。

$ git clone https://github.com/atijust/laravel-tricks.git
$ composer install
$ php artisan migrate
$ php artisan db:seed
$ php artisan serve

PHPの組み込みサーバが起動し、http://localhost:8000/でLaravel Tricksにアクセスできるようになります。このローカルホストで動いているLaravel Tricksにたいして受け入れテストを実行します。

Codeceptionのインストール

composerでcodeception/codeceptionをインストールします。

$ composer require "codeception/codeception:2.*"

テストに必要な各種ファイルが格納されたtestsディレクトリと、グローバル設定ファイルであるcodeception.ymlを生成します。

$ php vendor/bin/codecept bootstrap
Initializing Codeception in /Users/atijust/Develop/laravel-tricks

File codeception.yml created       <- global configuration
tests/unit created                 <- unit tests
tests/unit.suite.yml written       <- unit tests suite configuration
tests/functional created           <- functional tests
tests/functional.suite.yml written <- functional tests suite configuration
tests/acceptance created           <- acceptance tests
tests/acceptance.suite.yml written <- acceptance tests suite configuration
tests/_output was added to .gitignore
tests/_bootstrap.php written <- global bootstrap file
Building initial Tester classes
Building Actor classes for suites: acceptance, functional, unit
\AcceptanceTester includes modules: PhpBrowser, AcceptanceHelper
AcceptanceTester.php generated successfully. 48 methods added
\FunctionalTester includes modules: Filesystem, FunctionalHelper
FunctionalTester.php generated successfully. 13 methods added
\UnitTester includes modules: Asserts, UnitHelper
UnitTester.php generated successfully. 17 methods added

Bootstrap is done. Check out /Users/atijust/Develop/laravel-tricks/tests directory

以上でCodeceptionのインストールは完了です。

シナリオを作成

例としてログインのシナリオを作成してみましょう。

Laravel Tricksのログインの流れは次のようになります。

  1. /loginにアクセス
  2. ユーザ名とパスワードを入力
  3. LOGINボタンをクリック
  4. 投稿したトリックの一覧が表示されればログイン成功

これをシナリオに書き下してみます。

まずはシナリオを生成します。

$ php vendor/bin/codecept generate:cept acceptance Signin
Test was created in SigninCept.php

tests/acceptance/SigninCept.phpにひな形が生成されているのでシナリオを記述します。

<?php
$I = new AcceptanceTester($scenario);
$I->wantTo('log in as regular user');
$I->amOnPage('/login');
$I->fillField('#username','msurguy');
$I->fillField('#password','password');
$I->click('#wrap > div.container > div > div > div > form > div:nth-child(5) > button');
$I->see('My tricks');

なにをやっているかはだいたいわかるのではないでしょうか。

テストシナリオは自然言語(英語)に近いかたちで記述することができます。とはいってもDSLというわけではなく普通のPHPコードです。あくまでもそれっぽく記述できるというだけです*2

まず$I->amOnPage('/login');でログインページにアクセスします。

次に$I->fillField('#username','msurguy');でユーザ名、$I->fillField('#password','password');でパスワードをフォーム入力します。ユーザ名とパスワードはシーダーでデフォルトで用意されているものです。入力するフィールドはCSSパスで指定していますが他にもさまざまな指定方法が利用できます。

そして$I->click('#wrap > div.container > div > div > div > form > div:nth-child(5) > button');でLOGINボタンをクリック。ここでもCSSパスでLOGINボタンを指定しています。

ログインが成功したら投稿したトリックの一覧が表示されるのですが、その際に「My Tricks」という文字列が画面に出ているはずなので、それがあればログイン成功とみなします。

テストの実行

テストを実行してみましょう。

アプリケーションのURLをtests/acceptance.suite.ymlに設定します。

    config:
        PhpBrowser:
            url: 'http://localhost:8000/'

テストを実行します。

$ php vendor/bin/codecept run acceptance --steps
Codeception PHP Testing Framework v2.0.9
Powered by PHPUnit 4.4.0 by Sebastian Bergmann.

Acceptance Tests (1) -----------------------------------------------------------------------------------------------
Trying to log in as regular user (SigninCept)
Scenario:
* I am on page "/login"
* I fill field "#username","msurguy"
* I fill field "#password","password"
* I click "#wrap > div.container > div > div > div > form > div:nth-child(5) > button"
* I see "My tricks"
 PASSED

--------------------------------------------------------------------------------------------------------------------


Time: 715 ms, Memory: 13.25Mb

OK (1 test, 1 assertion)

成功したようです。

このようにしてCodeceptionを使えばブラウザを使ったテストを自動化することができます。ここでは非常な簡単な例のみで、使用したCodeceptionの機能も基礎的なものだけでしたが、受け入れテストを記述するための豊富な機能がCodeceptionには用意されています。スクショ撮ったりとかもできます。

以上、Laravel Advent Calendar 2014 22日目でした。

*1:オリジナルのLaravel TricksはPHPUnitのバージョンが低かったり、一部のCSSや画像ファイルのパス指定がおかしかったりするので、設定以外にそれらも修正しています。

*2:この辺の設計思想については公式ブログで解説させているので興味があれば参照ください。

Rocketeerを本格的に使うのに必要なちょっと進んだ機能

PHPのためのCapistrano風デプロイツール「Rocketeer」でLaravelをデプロイするの補足として、Rocketeerを本格的に運用する上で必要となってくるちょっと進んだ機能を紹介します。

複数のリモートホストを一括で操作する

app/config/packages/anahkiasen/rocketeer/config.phpconnectionsには複数のリモートホストの接続情報を設定することができます。

app/config/packages/anahkiasen/rocketeer/config.php

// The various connections you defined
// You can leave all of this empty or remove it entirely if you don't want
// to track files with credentials : Rocketeer will prompt you for your credentials
// and store them locally
'connections' => array(
    'sv1' => array(
        'host'      => '203.0.113.1',
        'username'  => 'hogehoge',
        'password'  => '',
        'key'       => '/Users/hogehoge/.ssh/id_rsa',
        'keyphrase' => '',
    ),
    'sv2' => array(
        'host'      => '203.0.113.2',
        'username'  => 'hogehoge',
        'password'  => '',
        'key'       => '/Users/hogehoge/.ssh/id_rsa',
        'keyphrase' => '',
    ),
),

各コマンドは--onオプションで操作対象のリモートホストを指定できます。

$ ./artisan deploy:deploy --on sv1
$ ./artisan deploy:rollback --on sv1

カンマ区切りで複数指定も可能です。

$ ./artisan deploy:deploy --on sv1,sv2
$ ./artisan deploy:rollback --on sv1,sv2

これで複数のリモートホストを一括して操作できますね。

ただ、毎回--onオプションを指定するのも面倒なので、そんなときはデフォルトで使用する接続情報を設定してやりましょう。 --onオプションが指定されないときは、defaultに設定した接続情報が使われます。

app/config/packages/anahkiasen/rocketeer/config.php

// The default remote connection(s) to execute tasks on
'default' => array(
    'sv1',
    'sv2',
),

こうしておけば--onオプションが指定されていないときは自動的sv1sv2の両方が操作対象になります。

$ ./artisan deploy:deploy

複数のリモートホストを扱う上で注意すべき点としては、リモートホストごとにリリースのタイムスタンプが変わるということでしょうか。

あと、Capistranoと違ってタスクは直列で実行されます。リモートホストの台数が多いと少し時間がかかってしまうかもしれません。

リモートホストごとにデプロイの設定を変える

本番とステージングでデプロイするブランチを変えたい、といった時はどうすればよいでしょうか。

例えば、productionstagingの2つのリモートホストがあって、productionではmasterブランチを、stagingではdevelopブランチをデプロイしたいとします。

そんなときは、app/config/packages/anahkiasen/rocketeer/config.phpon.connectionsでグローバルな設定値を上書きすればOKです。

app/config/packages/anahkiasen/rocketeer/config.php

// Contextual options
//
// In this section you can fine-tune the above configuration according
// to the stage or connection currently in use.
// Per example :
// 'stages' => array(
//  'staging' => array(
//      'scm' => array('branch' => 'staging'),
//  ),
//  'production' => array(
//    'scm' => array('branch' => 'master'),
//  ),
// ),
////////////////////////////////////////////////////////////////////

'on' => array(

    // Connections configuration
    'connections' => array(
        'production' => array(
            'scm' => array('branch' => 'master'),
        ),
        'staging' => array(
            'scm' => array('branch' => 'develop'),
        ),        
    ),

),

こうしておけば、productionを操作するときはscm.branchmasterstagingを操作するときはscm.branchdevelopに設定されます。scm.branchだけでなく、他の設定も上書きすることができますので、色々便利に使えそうですね。

1つのリモートホストに複数のリリースを同居させる

リモートホストを1台しか用意できないなんて場合は、本番用のリモートホストに動作確認用のステージング環境も同居させてしまいたい、なんていうことがあるかもしれません。

そんな場合はディレクトリを分けて1つのリモートホストに複数のリリースを同居させてしまいましょう。

app/config/packages/anahkiasen/rocketeer/stages.php

// Stages
//
// The multiples stages of your application
// if you don't know what this does, then you don't need it
//////////////////////////////////////////////////////////////////////

// Adding entries to this array will split the remote folder in stages
// Like /var/www/yourapp/staging and /var/www/yourapp/production
'stages' => array('production', 'staging'),

このように設定しておけば、

$ ./artisan deploy:deploy --stage production

とすれば/var/www/rocketeer-example/productionに、

$ ./artisan deploy:deploy --stage staging

とすれば/var/www/rocketeer-example/stagingにデプロイされます。

--stageオプションを省略したときのデフォルトを設定することもできます

app/config/packages/anahkiasen/rocketeer/stages.php

// The default stage to execute tasks on when --stage is not provided
'default' => 'production',

デフォルトが設定されてない状態で--stageオプションを省略するとproductionstaging両方がデプロイされてしまうので注意。

ステージ毎に設定を変更することもできます。

app/config/packages/anahkiasen/rocketeer/config.php

'on' => array(

    // Stages configurations
    'stages' => array(
        'production' => array(
            'scm' => array('branch' => 'master'),
        ),
        'staging' => array(
            'scm' => array('branch' => 'develop'),
        ),
    ),
),

これでproductionにはmasterブランチが、stagingにはdevelopブランチがデプロイされるようになります。

以上、Rocketeerを本格的に運用する上で必要なちょっと進んだ機能を紹介しました。他にもタスクをフックしてユーザ定義の処理を差し込んだり、オリジナルのタスクを書いたり、プラグインだったりといった楽しそうなトピックがありますが、まだ自分は使ったこと無いし、その辺の情報は公式ドキュメントが充実しているので、そちらを参照ください( 英語だけどコードを多用した説明なのでエンジニアならなんとかなるハズ)。

PHPのためのCapistrano風デプロイツール「Rocketeer」でLaravelをデプロイする

そろそろrsyncでデプロイするのは卒業したいな、ということでRocketeerというデプロイツールを導入してみました。

RocketeerはPHP製のCapistrano風デプロイツールです。PHP製なだけあってはじめからComposerやPHPUnitをサポートしてるし、当然だけど設定ファイルや新しいタスクもPHPで記述できるしでとても使いやすいです。

Rocketeer自体はフレームワークに依存しないデプロイツールではありますが、Laravelのパッケージとしてインストールすると、artisanからデプロイできたり、データベースのマイグレーションやシーディングなんかもできるようになるので、Laravelアプリケーションのデプロイには特に便利に使えます。

ただ、新興のツールであるからか、日本語での具体的な導入手順について解説している情報があまりありません。素晴らしいツールでありながら導入に障壁があるのももったいないので、このエントリではRocketeerでLaravelで作ったアプリケーションをデプロイする方法を紹介したいと思います。

Rocketeerの概要

RocketeerはSSHでデプロイ先のリモートホストに接続し、タスクとして定義されたコマンド群を実行することでデプロイを行います。ソースコードリモートホスト上でgitもしくはsvnを使って取得し、ライブラリはcomposerでインストールします。今のところローカルからファイルをコピーしてデプロイする機能はサポートされていないようです。

つまりRcoketeerを導入するには

の3つの前提をクリアしている必要があります。

次にデプロイしたアプリケーションのリモートホスト上でのディレクトリ構造について説明します。

Rocketeerの特徴の一つは、単にアプリケーションをデプロイするだけでなく、過去のリリースにロールバックする機能を持っていることでしょう。そのため、リモートホストでのディレクトリ構造は少々独特です。

リモートホストでの典型的なディレクトリ構造は次のようになります。

/var/www/rocketeer-example/
├── current -> /var/www/rocketeer-example/releases/20140116170238
├── releases
│   ├── 20140115152810
│   ├── 20140115173018
│   ├── 20140115174248
│   └── 20140116170238
│       ├── app
│       │   └── storage
│       │       ├── logs -> /var/www/rocketeer-example/shared/app/storage/logs
│       │       └── sessions -> /var/www/rocketeer-example/shared/app/storage/sessions
│       └── public
└── shared
    └── app
        └── storage
            ├── logs
            └── sessions

Rcoketeerはデプロイ先として指定されたパスにアプリケーション名のディレクトリを作成します。この場合は/var/wwwがデプロイ先のパスで、アプリケーション名はrocketeer-exampleです。

さらに/var/www/rocketeer-example/には3つのディレクトリが作成されます。

1つ目はcurrentです。ここにはアプリケーションの最新のリリースが設置されます。Webサーバはcurrent/publicを公開するように設定します。

2つ目はreleasesで、新しいものから4件(デフォルト値)のリリースが保存されます。currentは実際にはreleasesに保存さているリリースへのシンボリックリンクとなっており、currentのリンク先を変更することでデプロイをロールバックすることができるようになっています。

3つ目はsharedです。このディレクトリにはリリース間で共有する必要のあるファイルが置かれるディレクトリです。各リリースのディレクトリにあるログファイルなどはsharedにあるものにリンクされます。

Capistranoを使ったことがある人にはお馴染みでしょうが、シンボリックリンクを使ってリリースを切り替えるというのは目から鱗で面白いですね。

インストール

composerでanahkiasen/rocketeerパッケージをインストールします。

$ composer require anahkiasen/rocketeer:dev-master
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
  - Installing anahkiasen/rocketeer (dev-master 3591b74)
    Cloning 3591b74208e632ce5ffe090ffcd19330785feb77

anahkiasen/rocketeer suggests installing anahkiasen/rocketeer-campfire (Campfire plugin to create deployments notifications)
Writing lock file
Generating autoload files
Generating optimized class loader

app/config/app.phpproviders配列に次の行を追加。

'Rocketeer\RocketeerServiceProvider',

app/config/app.phpaliases配列に次の行を追加。

'Rocketeer' => 'Rocketeer\Facades\Rocketeer',

deploy:で始まるコマンド群が追加されます。

$ ./artisan
deploy
  deploy:check                Check if the server is ready to receive the application
  deploy:cleanup              Clean up old releases from the server.
  deploy:current              Display what the current release is
  deploy:deploy               Deploy the website.
  deploy:flush                Flushes Rocketeer's cache of credentials
  deploy:ignite               Creates Rocketeer's configuration
  deploy:rollback             Rollback to the previous release, or to a specific one
  deploy:setup                Set up the remote server for deployment
  deploy:teardown             Remove the remote applications and existing caches
  deploy:test                 Run the tests on the server and displays the output
  deploy:update               Update the remote server without doing a new release.

Laravel4.0の場合はremoteパッケージを別途導入する必要があります。4.0へのインストール方法は公式サイトで解説されていますので参照ください。

設定

具体的な設定を行っていく前に設定に必要な情報を確認しておきましょう。

デプロイ先のリモートホストついては次のように想定しています。

  • デプロイ先のパスは/var/www
  • /var/wwwのユーザとグループはwww-data
  • /var/wwwはセットグループIDされている
  • デプロイタスクは作業用ユーザ(hogehoge)で実行する
  • 作業用ユーザの補助グループをwww-dataにして/var/wwwにアクセスできるようにしてある
  • gitcomposerは予めインストール済み(デプロイタスクで使われる)

/var/wwwはセットグループIDされているので、作業用ユーザによってデプロイ時に作成されたファイルのグループはwww-dataになります。www-data:www-dataで動作しているWebサーバは、グループの権限を通じてファイルにアクセスできるという仕組みです。

アプリの名前はrocketeer-exampleで、Githubのパブリックなリポジトリhttps://github.com/hogehoge/rocketeer-example.git)で公開されているとします。

それでは具体的な設定を行っていきます。

まずはapp/config/remote.phpリモートホストの接続情報を設定します。 artisan tailが使えるようにrootには/var/www/rocketeer-example/currentを指定しておきましょう。

'connections' => array(

    'production' => array(
        'host'      => '203.0.113.1',
        'username'  => 'hogehoge',
        'password'  => '',
        'key'       => '/Users/hogehoge/.ssh/id_rsa',
        'keyphrase' => '',
        'root'      => '/var/www/rocketeer-example/current',
    ),

),

次にRocketeerの設定ファイルをpublishします。 アプリケーションのリポジトリと名前について聞かれるので入力します。

./artisan deploy:ignite
No repository is set for the repository, please provide one :https://github.com/hogehoge/rocketeer-example.git
Configuration published for package: anahkiasen/rocketeer
What is your application's name ?rocketeer-example
The Rocketeer configuration was created at anahkiasen/rocketeer
Execution time: 9.3387s

app/config/packages/anahkiasen/rocketeer/以下に設定ファイルが生成されます。 各設定ファイルには詳細なコメントがあるので、参考にしながら設定していきましょう。

app/config/packages/anahkiasen/rocketeer/
├── config.php
├── hooks.php
├── paths.php
├── remote.php
├── scm.php
└── stages.php

デプロイ先のディレクトリパスを指定します。

app/config/packages/anahkiasen/rocketeer/remote.php

// The root directory where your applications will be deployed
'root_directory'   => '/var/www/',

リリース間で共有されるべきディレクトリとファイルを指定します。SQLiteをプロダクションで使っているのでなければデフォルトで大丈夫だと思います。

app/config/packages/anahkiasen/rocketeer/remote.php

// A list of folders/file to be shared between releases
// Use this to list folders that need to keep their state, like
// user uploaded data, file-based databases, etc.
'shared' => array(
    '{path.storage}/logs',
    '{path.storage}/sessions',
),

Webサーバが書き込みするファイルやディレクトのパーミッションを変更するようにします(chmod -R755 %sだけでもいいですが、特に実害もないのでなんとなくデフォルトのまま他の2つのコマンドも残してあります)。アプリによってはapp/storageだけ書き込めるようになっていれば十分で、app/database/production.sqlitepublicは外してしまってもいいかもしれません。アプリの実装によって調整しましょう。

app/config/packages/anahkiasen/rocketeer/remote.php

'permissions' => array(

    // The folders and files to set as web writable
    // You can pass paths in brackets, so {path.public} will return
    // the correct path to the public folder
    'files' => array(
        'app/database/production.sqlite',
        '{path.storage}',
        '{path.public}',
    ),

    // Here you can configure what actions will be executed to set
    // permissions on the folder above. The Closure can return
    // a single command as a string or an array of commands
    'callback' => function ($task, $file) {
        return array(
            sprintf('chmod -R 775 %s', $file),
            sprintf('chmod -R g+s %s', $file),
            sprintf('chown -R www-data:www-data %s', $file),
        );
    },

パブリックなリポジトリを使うのでリポジトリのユーザ名とパスワードは空にしておきます。プライベートなリポジトリで認証が必要な場合はここでユーザ名とパスワードを指定します。

app/config/packages/anahkiasen/rocketeer/scm.php

// The repository credentials : you can leave those empty
// if you're using SSH or if your repository is public
// In other cases you can leave this empty too, and you will
// be prompted for the credentials on deploy
'username'   => '',
'password'   => '',

設定は以上です。

設定が完了したら./artisan deploy:checkを実行して、リモートホストがデプロイ可能な状態かチェックしておきます。

$ ./artisan deploy:check
No username is set for the repository, please provide one :
No password is set for the repository, please provide one :
Checking presence of git
Checking PHP version
Checking presence of Composer
Checking presence of mcrypt extension
Checking presence of mysql extension
The mysql extension does not seem to be loaded on the server
Execution time: 2.2685s

レポジトリにアクセスするためのユーザ名とパスを聞かれますが、パブリックなリポジトリを使っているので無視してEnterでOKです。

PHPのconfigureオプションの関係で、mysql extensionがないと怒られてますが実害はないので無視しています(app/config/database.phpで設定されているdatabase.defaultの値と同名のextensionが存在するかチェックするように実装されているのですが、これは本来はdriverを見て対応するextension名を決めるように実装したほうがいいと思う)。

デプロイ

deploy:deployコマンドでデプロイします。省略してdeployだけでもデプロイできます。

$ ./artisan deploy:deploy
No username is set for the repository, please provide one :
No password is set for the repository, please provide one :
Server is not ready, running Setup task
Checking presence of git
Checking PHP version
Checking presence of Composer
Checking presence of mcrypt extension
Checking presence of mysql extension
The mysql extension does not seem to be loaded on the server
Cloning repository in "/var/www/rocketeer-example/releases/20140114191141"
Initializing submodules if any
Installing Composer dependencies
Setting permissions for /var/www/rocketeer-example/releases/20140114191141/app/database/production.sqlite
Setting permissions for /var/www/rocketeer-example/releases/20140114191141/app/storage
Setting permissions for /var/www/rocketeer-example/releases/20140114191141/public
Sharing file /var/www/rocketeer-example/releases/20140114191141/app/storage/logs
Sharing file /var/www/rocketeer-example/releases/20140114191141/app/storage/sessions
Successfully deployed release 20140114191141
No releases to prune from the server
Execution time: 111.9915s

/var/www/rocketeer-exampleディレクトリが作成され、最新のリリースへのリンクが/var/www/rocketeer-example/currentに作成されます。Webサーバは/var/www/rocketeer-example/current/publicを公開するように設定すればOKです。

デプロイタスクの流れとしては

  1. 初回デプロイ時など必要なディレクトリが作成されてない場合はセットアップタスクを実行
  2. リポジトリをclone
  3. composer install
  4. --tests (-t)オプションが指定されていればphpunitを実行しテストが失敗したらデプロイを中止
  5. config.phpの設定に従いパーミッションを変更
  6. --migrate (-m)オプションが指定されていればartisan migrateを実行(--seedと併用された場合はartisan migrate --seed
  7. --seed (-s)オプションが指定されていればartisan db:seedを実行
  8. リリース間で共有されるフォルダにリンクを張る
  9. currentのリンク先を更新

となっています。

一般的なLaravelを使ったアプリケーションのデプロイとして必要なことはひと通りよしなにやってくれます。便利ですね。

ロールバック

deploy:rollbackコマンドでcurrentのリンク先を変更してデプロイをロールバックできます。

一つ前のリリースに戻す。

$ ./artisan deploy:rollback
Rolling back to release 20140114195555
Execution time: 2.4191s

ロールバック可能なリリースのリストを表示し、番号でロールバック先を選択。

$ ./artisan deploy:rollback --list
Here are the available releases :
[0] 2014-01-14 19:57:28
[1] 2014-01-14 19:55:55
[2] 2014-01-14 19:53:59
[3] 2014-01-14 19:11:41
Which one do you want to go back to ? (0)
Rolling back to release 20140114195728
Execution time: 95.3531s

バグが見つかったときも、すぐに以前のリリースに戻すことができるので安心です。

そのほかのコマンド

deploy:current

currentからリンクされているリリースを表示します。

./artisan deploy:current
The current release is 20140114191141 (b3f46005e0ec4b1e202a113c5fa31875ef772826 deployed at 2014-01-14 19:11:41)
Execution time: 1.6177s

deploy:update

currentからリンクされているリリースでgit pullcomposer installを実行します。

$ ./artisan deploy:update
Pulling changes
Sharing file /var/www/rocketeer-example/releases/20140115174248/app/storage/logs
Sharing file /var/www/rocketeer-example/releases/20140115174248/app/storage/sessions
Installing Composer dependencies
Setting permissions for /var/www/rocketeer-example/releases/20140115174248/app/database/production.sqlite
Setting permissions for /var/www/rocketeer-example/releases/20140115174248/app/storage
Setting permissions for /var/www/rocketeer-example/releases/20140115174248/public
Successfully updated application
Execution time: 8.2524s

deploy:cleanup

保存件数(デフォルトは4件)を超えた古いリリースをリモートホストから削除します。

$ ./artisan deploy:clean
Removing 1 release from the server
Execution time: 1.8961s

--clean-allオプションを使えば、currentからリンクされている以外のリリースをすべて削除することもできます。

$ ./artisan deploy:clean --clean-all
Removing 3 releases from the server
Execution time: 2.7766s

古いリリースはdeploy:deployが削除してくれるので、明示的にdeploy:cleanを実行する機会はあまりないとは思います。使いどころとしては、保存件数を減らした時とか、deploy:deployを中断してゴミが残ったときなどでしょうか。

deploy:teardown

リモートホストからアプリを削除します。/var/www/rocketeer-exampleが削除されます。

$ ./artisan deploy:teardown
This will remove all folders on the server, not just releases. Do you want to proceed ?
The application was successfully removed from the remote servers
Execution time: 3.9923s

deploy:flush

認証情報のキャッシュ(app/storage/metaあたりに保存されている)を削除します。設定の変更が反映されないときは取り敢えずこのコマンドを叩いてみる感じです。

$ ./artisan deploy:flush
Rocketeer's cache has been properly flushed

deploy:setup

デプロイ前にリモートホストにディレクトリを作成するのに使用します。明示的にこのコマンドを実行しなくても、deploy:deployコマンドが初回デプロイ時によしなにやってくれるので、直接使うことはあまりなさそうです。

deploy:test

リモートホストphpunitを実行します。

リポジトリSSHで接続する場合

HTTPSじゃなくてSSHリポジトリにアクセスすることもできます。その場合は.ssh/configを駆使して予めリモートホストの作業用ユーザでgit clone出来るように設定しておきましょう。.ssh/known_hostsリポジトリをホストしているサーバの公開鍵がないと、デプロイ時にエラーになるので、予めコマンドラインgit cloneを実行するなりしておくのも忘れずに。自分はこれでハマりました。

リモートホストの接続情報を変更するときの注意点

app/config/remote.phpの設定を変更するだけではダメです。

artisan deploy:igniteを実行したときに、app/config/remote.phpを元にしてapp/config/packages/anahkiasen/rocketeer/config.phpに接続情報が設定されるのですが、Rocketeerはconfig.phpに接続情報があればremote.phpより優先して使うので、接続情報を変更する場合は、remote.phpだけではなくconfig.phpも変更する必要があります。接続情報はキャッシュされているので、設定を変更したあとはartisan deploy:flushで接続情報のキャッシュをフラッシュしましょう。

もしくは、同じ内容の設定が2つのファイルにあるのが気持ち悪いのであれば、config.phpから接続情報を削除し、常にremote.phpを使うようにしてし まってもいいかもしれません。

実行されるコマンドを確認する

各コマンドに-pオプションを指定すると、リモートホストで実行されるコマンドを確認できます。これらのコマンドは実際に実行されるわけではありません。どのようなコマンドが実行されるのか不安なときはこれで確認を。-v|vv|vvvオプションでも色々見れます。

以上、RocketterでLaravelアプリケーションをデプロイする方法について簡単に紹介しました。ステージング環境用に設定を切り替えたり、複数台のサーバへの一括デプロイしたりといった本格的に運用する上で必要なことについて補足エントリを書きましたので、よろしければそちらも参照ください。

さくらVPS(CentOS6)にNginx+PHP5.5+MySQL5.6の環境を構築する(設定編)

インストール編の続きです。

動作確認用のLaravelを用意する

/var/www/testに動作確認用にLaravelをインストールします。

/var/www/testは所有ユーザ、グループともに作業用ユーザ(atijust)にし、nginxユーザの補助グループをatijustにすることで、NginxとPHP-FPM(nginxユーザ・グループで動作するように設定済み)からアクセスできるようにします。一般ユーザのumaskはデフォだと0002になってて、app/storageはatijustグループにも書き込み権限があるので、パーミッションを変更する必要はありません。

# usermod -G atijust nginx
# mkdir -p /var/www/test
# chown atijust:atijust /var/www/test
$ cd /var/www/test
$ composer create-project laravel/laravel --prefer-dist .

うーん、NginxやPHP-FPMに作業用ユーザのグループの権限を与えるのは微妙に気持ち悪いのでその辺りは要研究ですね。www-dataみたいなユーザとグループを作って、/var/www以下を所有させる。/var/wwwにセットグループIDして、nginxやatijustの補助グループをwww-dataにするとかかなぁ。ベストプラクティスが知りたい。まぁ、時間有るときに調べてみる感じで。

Nginxの設定

細かい設定はおいおいやるとして、取り敢えずLaravelが動くように最低限の設定を行います。

/etc/nginx/conf.d/default.conf

server {
    listen       80;
    server_name  203.0.113.1;

    root   /var/www/test/public;
    index  index.php;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        include /etc/nginx/fastcgi_params;
        fastcgi_pass 127.0.0.1:9000;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }
}

nginxに設定ファイルを再読み込みさせます。

# service nginx configtest
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
# service nginx reload

http://203.0.113.1/にアクセスしてみます(203.0.113.0/24はRFC6890の例示用アドレスです)。

f:id:atijusts:20140105185556j:plain

成功っぽい。

LaravelからMySQLが使えるかも試してみましたがこちらも問題なし。

以上、さくらVPSにNginx+PHP5.5+MySQL5.6の環境を構築してLaravelを動かしてみました。プロダクトとして使うにはまったくもって設定が適当過ぎますが、取り敢えず開発中のアプリをデプロイして遊ぶ分にはこれで十分でしょう。願わくばもうちょっと簡単にセットアップしたいので、その辺は今後の課題かな。

さくらVPS(CentOS6)にNginx+PHP5.5+MySQL5.6の環境を構築する(インストール編)

さくらVPS(CentOS6)にNginx+PHP5.5+MySQL5.6の環境を構築してLaravelを動かすまでの記録を備忘録として残しておきます。

Nginxのインストール

公式サイトのインストール方法の説明に従って、公式のyumリポジトリからStable版をインストールします。

まずはCentOS6用のyumリポジトリのセットアップ。 レポジトリの設定ファイルとPGPキーがインストールされます。

# rpm -ivh http://nginx.org/packages/centos/6/noarch/RPMS/nginx-release-centos-6-0.el6.ngx.noarch.rpm

次にyumでnginxのパッケージをインストール。

# yum install nginx

バージョンとビルドのオプションを確認します。

# nginx -V
nginx version: nginx/1.4.4
built by gcc 4.4.7 20120313 (Red Hat 4.4.7-3) (GCC)
TLS SNI support enabled
configure arguments: --prefix=/etc/nginx --sbin-path=/usr/sbin/nginx --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --pid-path=/var/run/nginx.pid --lock-path=/var/run/nginx.lock --http-client-body-temp-path=/var/cache/nginx/client_temp --http-proxy-temp-path=/var/cache/nginx/proxy_temp --http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp --http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp --http-scgi-temp-path=/var/cache/nginx/scgi_temp --user=nginx --group=nginx --with-http_ssl_module --with-http_realip_module --with-http_addition_module --with-http_sub_module --with-http_dav_module --with-http_flv_module --with-http_mp4_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_random_index_module --with-http_secure_link_module --with-http_stub_status_module --with-mail --with-mail_ssl_module --with-file-aio --with-ipv6 --with-cc-opt='-O2 -g -pipe -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector --param=ssp-buffer-size=4 -m64 -mtune=generic'

現時点での最新Stable版が入りました。ひと通りのモジュールは有効になっているみたいですね。設定ファイルは/etc/nginx/nginx.confのようです。あと、/etc/logrotate.d/nginxにlogrotateの設定が追加されて、/var/log/nginx/*以下をdailyでローテートするようにしている模様。

MySQL5.6のインストール

公式サイトの説明に従ってビルド済みのバイナリをインストールします。

MySQL公式のyumリポジトリからインストールする場合は、すでにインストールされているディストリ標準のmysql関連のパッケージ(mysql-libsとか)を削除する必要があるのですが、それらに依存しているパッケージの扱いをどうすればいいのかわからなかったので、今回は公式バイナリからインストールすることにしました。mysql-community-libs-compatという5.1互換のライブラリなんかもあるみたいなので、なんとかなりそうではあるのですが、まぁ、無難に行くということで。

まずは、ダウンロードページからLinux - Generic (glibc 2.5) (x86, 64-bit), Compressed TAR Archiveを取ってきます。

# wget http://dev.mysql.com/get/Downloads/MySQL-5.6/mysql-5.6.15-linux-glibc2.5-x86_64.tar.gz

公式サイトにある通りにコマンドを実行。

# groupadd mysql
# useradd -r -g mysql mysql
# cd /usr/local
# tar zxvf ~/mysql-5.6.15-linux-glibc2.5-x86_64.tar.gz
# ln -s mysql-5.6.15-linux-glibc2.5-x86_64 mysql
# cd mysql
# chown -R mysql .
# chgrp -R mysql .
# scripts/mysql_install_db --user=mysql
# chown -R root .
# chown -R mysql data

ディストリの/etc/my.cnfでdatadirとか勝手に設定されててそのままじゃ立ち上がらないので、取り敢えず適当に置き換え。

# mv /etc/my.cnf /etc/my.cnf.bak
# cp my.cnf /etc/my.cnf

立ち上げます。

# bin/mysqld_safe --user=mysql &

mysql_secure_installationを実行。

# bin/mysql_secure_installation

起動時に自動的に立ち上がるようにする。

# cp support-files/mysql.server /etc/init.d/mysql
# chkconfig --add mysql
# chkconfig | grep mysql
mysql           0:off   1:off   2:on    3:on    4:on    5:on    6:off

PHP5.5のインストール

ソースからビルドします。

remiから入れたほうが楽かもしれないけど、PHPは自分でビルドしたい気分なのです。

現時点で最新版の5.5.7のソースをダウンロードして展開。

$ wget http://jp1.php.net/get/php-5.5.7.tar.bz2/from/this/mirror
$ tar xf php-5.5.7.tar.bz2

必要なパッケージをインストール。

# yum -y install libxml2-devel libmcrypt-devel libcurl-devel libicu-devel pcre-devel openssl-devel 

configureを実行します。 オプションは取り敢えずLaravelとSymfonyを動かすのに必要そう&自分が使いそうなのを最小限指定してます。 実際に使ってみて不足するようならそのとき追加すればいいかな、という方針。

$ cd php-5.5.7
$ ./configure --enable-fpm --with-openssl --with-curl --enable-intl --enable-mbstring --with-mcrypt --enable-opcache --with-pdo-mysql

ビルド&インストール。 3コアのインスタンスなのでmakeは3並列で。

$ make -j 3
# make install

php.iniをコピーします。

# cp php.ini-production /usr/local/lib/php.ini

php-fpmの設定ファイルを用意します。

# cp /usr/local/etc/php-fpm.conf.default /usr/local/etc/php-fpm.conf

pidとログのパスを/var以下に変更。ユーザとグループをnginxに。 pmとかはおいおいベンチ取ってチューニングする方向で。

/usr/local/etc/php-fpm.conf

pid = /var/run/php-fpm.pid
error_log = /var/log/php-fpm.log
user = nginx
group = nginx

PHP-FPM用のinit.dスクリプトを/etc/init.d/にコピーしてパーミッションを変更します。

# cp sapi/fpm/init.d.php-fpm /etc/init.d/php-fpm
# chmod 755 /etc/init.d/php-fpm

pidのパスだけ修正。

/etc/init.d/php-fpm

php_fpm_PID=/var/run/php-fpm.pid

chkconfigで登録。

# chkconfig --add php-fpm
# chkconfig | grep php-fpm
php-fpm         0:off   1:off   2:on    3:on    4:on    5:on    6:off

init.dスクリプトについてはマニュアルのFPMの項目に付いたコメントを参考にしました。

インストールとしてはこんなところです。

仕事では最近はインフラ担当が全部用意してくれるので、自分でサーバーを弄ることもないのですが、久しぶりにやってみるとやっぱ大変ですね。。

設定編(そのうち公開)につづく。

さくらVPSを借りて作業用ユーザでSSHできるようにするまで

個人でお遊びで使うにはEC2はコスパが微妙なので、さくらVPSを借りることにしました。rootでのログインを禁止し作業用ユーザで公開鍵認証を使ってログインできるようにするまでを備忘録としてメモしておきます。OSはCentOS6(64bit)です。

rootでログインする

仮登録が完了するとサーバのIPとrootアカウントのパスワードを記載したメールが届きますので、まずはrootでログインします。

$ ssh 203.0.113.1 -l root
root@203.0.113.1's password:
Last login: Fri Jan  3 10:54:47 2014 from 203.0.113.2

SAKURA Internet [Virtual Private Server SERVICE]

[root@www7777up ~]#

rootのパスワードを変更します。

[root@www7777up ~]# passwd
ユーザー root のパスワードを変更。
新しいパスワード:
新しいパスワードを再入力してください:
passwd: 全ての認証トークンが正しく更新できました。

作業用ユーザの作成

作業用のユーザを作成します。su、sudoできるように補助グループをwheelにしています。

[root@www7777up ~]# useradd -m -G wheel atijust
[root@www7777up ~]# passwd atijust
ユーザー atijust のパスワードを変更。
新しいパスワード:
新しいパスワードを再入力してください:
passwd: 全ての認証トークンが正しく更新できました。

wheelグループにsu、sudoを許可。

[root@www7777up ~]# visudo
%wheel  ALL=(ALL)       ALL

作業用ユーザに公開鍵認証でログインできるように設定

先ほど作成した作業用ユーザに公開鍵認証でログインできるように公開鍵を登録します。GitHub に登録した SSH 公開鍵は全世界に公開されているとのことなので、公開鍵はGithubから取ってくるようにすれば簡単です。

[atijust@www7777up ~]$ curl https://github.com/atijust.keys --create-dirs -o .ssh/authorized_keys
[atijust@www7777up ~]$ chmod 700 .ssh
[atijust@www7777up ~]$ chmod 600 .ssh/authorized_keys

一旦ログアウトして作業用ユーザでログインできるか確認します。

$ ssh 203.0.113.1 -l atijust

SAKURA Internet [Virtual Private Server SERVICE]

[atijust@www7777up ~]$

rootでのログインとパスワード認証を禁止する

rootにSSHでログインできないようにします。またパスワードでのログインもできないようにします。ポートが22番のままだとアタックを受けて気持ち悪いのでこれも適当な番号に変更しておきます。

[root@www7777up ~]# vi /etc/ssh/sshd_config
PermitRootLogin no
PasswordAuthentication no
Port 10022
[root@www7777up ~]# /sbin/service sshd restart

以後、サーバへログインするときは作業用ユーザで公開鍵認証を使うようにして、rootでの作業が必要な場合はsudoを使うようにします。

取り敢えず、初期設定としてはこんな感じ。

Laravelでテンプレート用のロジックを整理する方法

Laravel Advent Calendar 2013の15日目です。 robclancy/presenterでテンプレート用のロジックを整理する方法を紹介します。

テンプレート用ロジックの置き場所の問題

Laravelに限らずですが、Webアプリを開発していてぶち当たる問題として、テンプレート用のロジックをどこに置くかというのがあります。

例えばモデルの作成日時をテンプレートで表示するというのはよくありますよね。

{{{$article->created_at->format('M d, Y')}}}

何も考えずに書くとこんな感じかな。テンプレート内でフォーマットを指定して表示しています。 うん問題ない。でも、日付の表示がここ1箇所だけならいいのですが、他に何箇所も表示しなければならないとしたら、ちょっと嫌ですよね。DRYじゃない。表示のフォーマットを変えたくなった場合、すべての箇所を1つずつ修正しなければなりません。

そこで、日付をフォーマットしてくれるヘルパーを用意してみます。

<?php
function article_date($date)
{
    return $date->format('M d, Y');
}
{{{article_date($article->created_at)}}}

これなら、フォーマットを変えたくなった時もヘルパーを修正するだけで大丈夫。やったね! …でも、ArticleだけならまだしもCommentやらなんやらの日時も表示するとなると、それに対応するヘルパーが際限なく増えていくわけで、なんか嫌かも。グローバル関数がいっぱいとかゾッとしますよね。

であれば、モデルに書いちゃえ!

<?php
class Article extends Eloquent
{
    public function formatDate()
    {
        return $this->created_at->format('M d, Y');
    }
}
{{{$article->formatDate()}}}

いやいやいやモデルにテンプレート用のロジックとかあり得ないだろjk

…このように、テンプレート用のロジックの置き場所というのは悩ましいものです。

それrobclancy/presenterで上手くできるよ!

少し前の自分なら、もやもやした気分のままヘルパーをひたすら増やすか、えいやでモデルにメソッドを追加していたでしょうが、最近いいパッケージを知りました。robclancy/presenterです。このパッケージを使えば、Decoratorパターンでモデルにメソッドを追加できます。

<?php
class ArticlePresenter extends Robbo\Presenter\Presenter
{
    public function formatDate()
    {
        return $this->created_at->format('M d, Y');
    }
}

// …

$article = new ArticlePresenter($article);
{{{$article->formatDate()}}}

このようにテンプレート用のロジックを実装したPresenterを用意し、モデルをラップすることで、モデルのクラスを変更することなくメソッドを追加することができます。Presenterは自身にないメソッドやプロパティへのアクセスは、モデルに自動的に移譲してくれるので、元々のモデルのメソッドも問題なく使えます。

<?php
class Article extends Eloquent implements Robbo\Presenter\PresentableInterface
{
    public function getPresenter()
    {
        return new ArticlePresenter($this);
    }
}

また、モデルにPresentableInterfaceを実装することで、テンプレートにモデルを渡すときに自動的にPresenterでラップしたものに差し替えることもできます。Controllerで手動でちまちまラップする必要がなくて便利です。

これで、テンプレート用のロジックをキレイに実装できるようになって、気持よくプログラミングに励むことができますね!

最後になりましたが導入方法です。 composer.jsonrobclancy/presenterを追加します。

 "robclancy/presenter": "1.1.*"

app/config/app.phpでServiceProviderを登録します。 このとき必ず Illuminate\View\ViewServiceProviderより後になるようにしましょう。

<?php
 'Illuminate\View\ViewServiceProvider',
 // …
 'Robbo\Presenter\PresenterServiceProvider'

robclancy/presenter自体はLaravel以外でも使える汎用的なライブラリですので、Laravel以外のフレームワークと組み合わせてみるのもいいかもしれませんね。

以上、Laravel Advent Calendar 2013の15日目でした。 明日の担当はHiroKwsさんです。よろしくお願いします!