Hina-Mode

とある呑んだくれエンジニアの気が向いた時に書く戯言

FuelPHPの非同期処理パッケージを(ちょっと前に)自作したので改めて紹介します

FuelPHP Advent Calendar 2015 の17日目を担当します @hinashiki です。よろしくお願いします。
先日は12日目の記事も担当させていただきました。

去年、今年あたりにかけて、FuelPHPのパッケージを幾つか作成したので、一つだけ簡単にさせていただきます。

作成したものはFuelPHPで動かす非同期処理のパッケージです。
既に同様のパッケージについては他の方も作っていらっしゃったのですが

  • 自分のアプリ要件的にbeanstalkを入れるのが難しかった
  • Daemon化するのに別ファイルが必要だった

などの理由から見送らせて頂き、結局自作することにしました。

概要

実際のパッケージはこちらです。
packagistにも入ってますのでcomposerでinstallできます。
一応必要な案内は一通り(拙いですが)README.mdに記載してあります。

尚、利用前提条件として

  • ORMパッケージが利用出来る
  • DBが利用できる(MySQLでのみ、現状は動作確認してます)
  • Cronが利用できる(非同期処理の定期実行のため)

が必要となっています。

使い方

キューの挿入はModel_TaskQueueのsave_queueを利用します。

\Model_TaskQueue::save_queue(
    "Static::method",
    array($arg1, $arg2 ...)
);

第一引数にはstaticなクラスメソッド名を、第二引数には該当メソッドに渡す引数を配列で渡します。
その他の引数は後ほど説明します。
データはDBにINSERTされます。

で、実際のキューの実行はtaskとして処理します

php oil refine queue

実行時、溜まっているキューをDBのID順に引っ張って処理します。
1 taskにつき処理するキューは1つです。

並列処理したい場合にはcronで数秒おきに実行をしたり、複数サーバで処理したりすると良いかもしれません。
例)6秒おきにキュー処理したい場合(1分10回処理)

for i in {1..10}; do FUEL_ENV=production php oil refine queues &> /dev/null & sleep 6; done

オプション説明

上記は飽く迄基本処理であり、「もう少し細かい設定をしたい」場合のオプションが幾つか存在します。
3つほどピックアップして説明させていただきます。

並列処理の限界数指定

先の例の用に数秒に1回処理を抽出すると、サーバ側でタスク処理だけでパンクしてしまう事も、当然あります。
そこで本パッケージでは「1サーバで同時に動かせる限界数」を設定しています。
例) 同時処理限界数が3の時、4つめを動かした時の挙動

$ php oil r queue &
$ php oil r queue &
$ php oil r queue &
$ php oil r queue 
queue limit over.
$ 

という感じで、処理が開始される前に現在処理しているキュー数を確認し、負荷調整を行うことができます。

キューの優先順位の設定

キューには優先順位がつける事が出来ます。

Model_TaskQueue::save_queue() の第四引数で指定ができ、デフォルトは100です。
仮に優先処理したいキューに99などをあてておくと、デフォルトより優先的に処理する事が可能です。

キューの種別単位の同時実行数制限

Aの処理は2つ同時に実行したくない、Bの処理は4つまでしか実行させたくない、などの複数要件が重なる際に有効なオプションです。

config/queue.php に duplicate_type という指定ができ、

'duplicate_type' => array(
  1 => 1, // Aの処理
  2 => 4, // Bの処理
),

と設定すると、Aの処理は1つ、Bの処理は4つの同時処理が限界となります。
※この設定値は並列処理の限界数指定の影響を受けるため、限界値の設定はそれぞれ調整する必要があります。

尚、この種別設定は Model_TaskQueue::save_queue() の第三引数で指定できます。

まとめ

という感じでざっくりしたパッケージを造りました。
パッケージ作るというと
「全員に見られるし恥ずかしい」
「突っ込まれるの怖い」
「後々までの運用に責任持てない」
という不安もあるかと思いますが、出せばどうにかなるという適当感で私は出しちゃってます。

で、こうして外に出しておくと、自分の他の案件の作成時に使い回せますし、
「あー、こういうのないかなぁ」という他の人達の検索にももしかしたらヒットして、そこで役立つこともあるかもしれないので
どんどん外に出していきましょう(個人的にも楽したいのでそうして欲しいです(笑



この投稿は FuelPHP Advent Calendar 2015 の 17日目の記事です。

【FuelPHP】ORMパッケージのObserver機能を使ってユーザ作成時にIPを自動付与する

FuelPHP Advent Calendar 2015 の12日目を担当します @hinashiki です。よろしくお願いします。
普段は少人数で自社サイト運営をしながら呑んだくれたりマンガ読んだりアニメ観たりしております。

今回は掲題にあるとおり、DBへのレコード挿入時にObserver使えば便利だよ、という事を簡単に紹介します。

そもそもObserverってなんぞや?

ざっくりと説明しますと、DBへのINSERT, UPDATE時に何かしらのフックをいれることが出来る機能のことです


はじめに - Obervers - Orm パッケージ - FuelPHP ドキュメント

イベントベースのシステムは、特定のイベントに動作を追加できるようにします。 イベントが観測されると ORM は自動的にそれらを行うためには何があるのかどうかを確認するために追加されたすべてのオブザーバを呼び出します

と書いてありますが、要するに

  • INSERTの前後
  • UPDATEの前後

に特定動作を追加できるよーってことです。

では具体的にどんな感じで使うのか

上記の機能を使うためにはORMパッケージを入れる必要があるので、configファイルから設定を追加しましょう。

'always_load' => array(
    'packages' => array(
        'orm',
    ),
),


さて、では実際にコード記述に入ります。
試しに「usersテーブルにINSERTをしたら作成日時が自動的に保存される」という部分を作ってみます。

Modelの用意

fuel/app/classes/model/user.php を用意します。

class Model_User extends \Orm\Model
{
	protected static $_observers = array(
		'Orm\\Observer_CreatedAt' => array( // 実行内容が記述されているクラス名
			'events' => array('before_insert'), // INSERTの前に実行する
			'mysql_timestamp' => true, // php側の時刻ではなくmysql側の時刻を参照する
			'property' => 'created_at', // 保存するテーブルのカラム名
		),
	);

	protected static $_table_name = 'users'; // 保存するテーブル名
}

Orm\ModelクラスをextendsしたModelクラスを用意し、usersテーブルにはcreated_atカラムを用意します。

実行

あとはこのModelを使ってINSERTをすればOKです。

# Modelを使ってINSERT
$new = new \Model_User();
$new->property = 'something';
$new->save();

# DBクラスを使ってINSERTするとcreated_atには何も入りません。注意。
$query = DB::insert('users');

もう一歩進んで、本題

本題に戻りまして、IPアドレスの自動挿入についてご説明しましょう。
先ほどの「timestampを突っ込む」という本処理を実行していたのはOrmパッケージが持つObserver_CreateAtクラスでした。
では今度は「ip_addressを取得する」という本処理を自分で書いて持ってみましょう。

Observer_IpAddressの用意

fuel/app/classes/observer/ipaddress.php を用意します。

class Observer_IpAddress extends Orm\Observer
{
	public static $property = 'ip_address'; // デフォルトのカラム名
	protected $_property; // カラム名保持用プロパティ

	/**
	 * プロパティ名の変更があったらここで出来るよ的な処理
	 */
	public function __construct($class)
	{
		$props = $class::observers(get_class($this));
		$this->_property = isset($props['property']) ? $props['property'] : static::$property; 
	}


	/**
	 * 各イベント名機能。ここではINSERT前に取得したいのでbefore_insert()としてます
	 */
	public function before_insert(Orm\Model $Model)
	{
		if(is_null($Model->{$this->_property}))
		{
			$Model->set($this->_property, \Input::real_ip()); // IPアドレス入れ込む本処理
		}
	}
}
Modelへの記述追加

Observerを作成したら、先ほどのModel_Userの$_observersにObserver_IpAddressを追加してあげます。

	protected static $_observers = array(
		'Orm\\Observer_CreatedAt' => array(
			'events' => array('before_insert'),
			'mysql_timestamp' => true,
			'property' => 'created_at',
		),
		// 追加
		'Observer_IpAddress' => array( // 実行内容が記述されているクラス名
			'events' => array('before_insert'), // INSERTの前に実行する
			'property' => 'ip_address', // 保存するテーブルのカラム名
		),
	);

これで準備は完了です。
あとはModel_Userを通じてsaveを実行してあげれば、IPアドレスがusers.ip_addressに追加されます。

まとめ

いかがでしたでしょうか。Observerは他にも

  • 直前にValidationを実行する
  • 投稿された座標情報を自動取得して保存しておく
  • 論理削除用にとりあえずdeleted = 0を仕込んでおく

など、色々な使い方が出来ます。覚えておくと便利な機能だと思いますので是非活用してみてください。




この投稿は FuelPHP Advent Calendar 2015 の 12日目の記事です。

【2015-07-04 修正有】FuelPHPのPresenter、ViewModelではクロージャを作成してはイケない。

これでちょっとハマりましたので注意喚起の意味も込めてエントリー生成します。
※注意喚起がメインなのでタイトルは若干あおり気味ですがご理解ください。

ビューモデル - 概要 - FuelPHP ドキュメント

class View_Index extends ViewModel
{

    public function view()
    {
        $this->echo_upper = function($string) { echo strtoupper($string); };
    }
}

$echo_upper('this string'); // 出力: "THIS STRING"

いわゆるViewModel、最近ではPresenterという形で実装されているコチラですが、
サンプルではクロージャを作成し、Viewに持っていけるような雰囲気で記載されています。

が、この記述、バージョン1.7.3以降では動作しません。

github.com

上記のコミットでクロージャをviewの変数として渡すとrender時に実行されて、render時の実行結果がviewの変数として格納されるように仕様が変更されています。
よって、View側でクロージャを実行しようとするとCall to undefined functionでFatal Errorが発生します

というわけで現状ではFuelPHPではviewへクロージャを引き渡す方法が公式では存在しないようなのでご注意下さい。

修正

github.com

github.com

id:Kenji_sさんが再度本家でissue発行して下さいました。ありがとうございます。
修正コミットも既に反映されているようです。
1.8/develop以降で改めてクロージャが利用できるようになっています
1.8以降でViewでクロージャを利用する方法は2通り存在するようです。

1つ目は下記のようにset_safeを用いる事でクロージャをその場で実行しないように回避する方法。

class View_Index extends ViewModel
{

    public function view()
    {
        $this->set_safe('echo_upper', function($string) { echo strtoupper($string); });
    }
}

$echo_upper('this string'); // 出力: "THIS STRING"

2つ目はconfig.php側でfilter_closuresをfalseに設定する方法のようです。

http://fuelphp.com/dev-docs/general/presenters.html#/functions


※1.7.3では相変わらずこの仕様は変わっていないため、引き続き利用にはご注意ください。
該当バージョンの利用時には該当コミットをパッチとして充てることを推奨します。

AWSのEC2+ELBで、ロードバランサへのアクセス時点で特定IPからのアクセスを弾く

つい先日、どこぞのサーバからEC2のサーバに対してDosアタックを受けました。
別にある程度の攻撃を受けること自体は想定してサーバを公開していましたし、
Apache側ではmod_dosdetectorというDos攻撃をある程度受けた場合、検知出来る仕組みを用意していたので特に問題ない、、、と思っていました。

stanaka/mod_dosdetector · GitHub

が、しかし。
Apache側で検知するということは、WEBサーバのリソースを結構食っちゃうんですよね…。

実際、普段5%以内で収まっていたCPU Usageが、攻撃を食らっている間50%オーバーをずっとマークしていました。

f:id:hinashiki:20150219153950p:plain

真ん中の跳ねてるグラフが攻撃受けてた期間ですね。

さすがに運営に支障が出るレベルだったので、出来ればWEBサーバの前に立っているELBにアクセス来た時点で、対象の攻撃者IPを弾いて欲しかったんですが
AWS側で用意されているSecurity Groupsはホワイトリスト形式での指定は可能なのですが、ブラックリスト形式での指定が出来ないため途方に暮れていました。

Google先生に「aws ロードバランサ ip拒否」とか聞いてみたんですが、
求めていた一発回答は存在せずに結構悩みました。
が、上記で出てきたDevelopers.io様の記事の中に

[Apache] 非VPCのELBでX-Forwarded-Forを利用したアクセス制限 | Developers.IO

VPC環境で、ELB配下のWebサーバがインターネットからのDoS攻撃にさらされた場合に、特定のIPアドレスからのアクセスを拒否(403)します。
VPC環境であればNetworkACLやSecurityGroupで制限をかけることができますが、非VPCではELBへのアクセスを制限することができません。この場合httpdレベルで制限する必要があります。

という内容が書かれていたのを見つけました。

…ん?NetworkACL、かけれるの?

という今更知りました感満載の内容を見つけ、今度は「AWS ACL」で検索。

http://docs.aws.amazon.com/ja_jp/AmazonVPC/latest/UserGuide/VPC_ACLs.html

ありました。

今まで全く気にしてもいなかったんですが、かけれるんですね。

ということでまずはELBのVPC-IDを確認。

f:id:hinashiki:20150219155629p:plain

みつけました。これを今度はVPC画面で確認します。

f:id:hinashiki:20150219155836p:plain

で、左のナビゲーションからNetwork ACLsを選択。

f:id:hinashiki:20150219160311p:plain

恥ずかしながら今まで、
VPC-IDって何やねんそれ、おまじないか?」
ぐらいにしか思ってなかったのでVPC-IDは1つしか無いです。

Editを押して編集に入ります。

f:id:hinashiki:20150219160605p:plain

ルール番号は100がデフォルトでAll RangeのAllowを行っているみたいです。
番号が若いほど優先処理をされるようなので、今回はとりあえず直前の99で該当IPの拒否処理をしようと思います。
※画像にIPおもいっきり乗っかってますが、攻撃者のIPなんて晒されても何も文句ないと思うので放置です。海外みたいですし。

最後にSaveを押せば、特にELBの再起動とか必要なく、リアルタイムでACLがかかりました。


とりあえず一時しのぎで手動で対応しましたが、最終的にはDosアタック検知したら自動でACLで弾いてしまうようにしたいですね。。。

というかこれ以外でスマートなやり方をご存知な方がいらっしゃったら是非ご教授願いたいです。

FuelPHPでメンテナンスモードパッケージを作成してみた

hinashiki/fuelphp-maintenance · GitHub

とある複数サイトで独自の503ルールが必要になったため、せっかくだからとパッケージにしてみました。

fuel/app/config/maintenance.php

<?php
return array(
  "maintenance_mode" => true
);

を作成し、Contoller::before()へ

\MaintenanceMode::check();

を入れれば、アプリケーション全体がメンテナンスモードになります。
メンテナンスモード中はステータス503を常に返し、返されるビューは固定されます。

capistranoなどのdeployで利用したい場合はfalseのファイルも持っておいてファイルを入れ替えれば素敵なメンテナンス切り替えが可能になります。

一応パッケージ内にデフォルトのビューを用意していますが、自分で作成したい場合は
fuel/app/views/503.php
を作成していただければ大丈夫です。

また、503.php以外をビューに利用したい場合は

fuel/app/config/maintenance.php

<?php
return array(
  "maintenance_mode" => true,
  "view" => "hogehoge"
);

という感じでhogehogeにビューパスを指定していただければ変更できるようになっています。

また、メンテナンスモードに移行するのにいちいちdeployカマしたくないというずぼらな方の為に、 503を強制throwできるようにしています。

throw new \HttpServerMaintenanceException();

を投げれば、メンテナンスモードの画面を返します。

外部DBやAPIへの接続に失敗した場合にthrowするようにすれば、該当APIなどを停止するだけでサイト側も自動でメンテナンスモードへ移行し、HTTPステータス503を返すことが出来ます。

メンテナンスモードを実装するには他にも

FuelPHP でシンプルメンテナンスモード - Qiita

上記のような方法があるのですが、

コントローラーまで処理が渡らないので、コントローラーが必要な処理は出来ない

ということでしたので、まぁ使い分けが出来ればいいかなー位にサクッと作りました。

どこかで役に立てば幸いです。