AirGram で Android や iPhone に PUSH 通知

先月、かつて社内勉強会と呼ばれた何か(仮)で話したスライドがあったので晒しておきます。

諸事情でスクリプトから自分の携帯に PUSH 通知したくなったことがあり、そのときに知ったサービスです。

ネイティブアプリではない Web サービスでもモバイルへの PUSH 通知を実装できる、ということらしいですが・・・

$ curl https://api.airgramapp.com/1/send_as_guest \
  --data-urlencode email='oreore@example.com' \
  --data-urlencode msg='Hello world!' \
  --data-urlencode url='http://example.com'
curl: (51) SSL: certificate subject name '*.herokuapp.com'
  does not match target host name 'api.airgramapp.com'

証明書が invalid です。

つまりもうメンテされていないということだと思うので、使わないほうがいいと思います。

単に携帯に通知したいだけならメール投げるだけでもいいし(即時性は劣りますが)、Slack に通知するとか、Twitter で自分にダイレクトメッセージを送るとか、他に方法はいくらでもありますし。

xip.io で簡単バーチャルホスト

先日、かつて社内勉強会と呼ばれた何か(仮)で xip.io というものについて話しました。

そんときのスライドはこちら。

PowerDNS といういろいろなものをバックエンドに使える DNS サーバで作られた、IP アドレスをサブドメインに入れておくとその IP アドレスそのものに名前解決されるドメインです。

例えは、以下のような URL でローカルホストにアクセスできます(127.0.0.1 に解決される)。

開発用途とかでたくさんバーチャルホストが欲しいとき、よく /etc/hosts とかを弄ることがあったと思いますが、代わりに使えるかもしれません。

まあ社内なら最近は社内用のドメインDNSサーバがあって、わりとカジュアルにワイルドカード CNAME を登録できるので、わたしはあんまり使う機会ないかも。

Ansible おれおれユースケース

先日、社内勉強会改め、おれとわたしと仲間たち勉強会(仮)で Ansible のことを話しました。

ユースケースの紹介をしたかったというか、

「ホスト一台の構成でも Ansible 便利だから使っていくよ!」

という宣言がしたかっただけなのですが、それだけだと寂しかったので 最後のケースを無理やり突っ込みました。

Ansible をゆるふわに使う

先日、社内勉強会のようななにかで Ansible のことを話しました。

当初は、role を使わずにプレイブックからプレイブックを include する方が簡単なのでよほど大きな構成にならない限りは role は使わないほうが良いよー

というコンセプトだったのですが、業務で Ansible を使っているうちに role の方がいろいろ捗るような気がしてきたので途中で方向性を変えました。

プレイブック1枚だけで完結する程度であれば role は不要かもしれませんが、分割したくなったらとりあえず role にしておけばいいと思います。


最後の方に書いていますが、あんまり複雑なことはしないほうが良いです。じゃないと、

  • 手段と目的が逆転する
    • Ansible を使うことそのものが目的になってる
  • メリットが感じられなくなってモチベーションがダダ下がり
    • プレイブックのメンテにかけるトータルの時間より手作業の方が短くね?
    • そんな頻繁にサーバを再構築しないですしおすし

となり、やる気がゴリゴリ削がれていきます。

shell モジュール(とか command とか)はどうしても使わざるをえないことはありますが、ファイルの中身を弄くる系のモジュールはめんどくさいのでなるべく使わないほうが良いです。

また、今年のはじめの頃の Jenkins カンファレンスに参加したときに誰かが言っていましたが、

自動化であまり複雑なことをするとむしろ属人性を高める

と思います。

ssh で接続して vim である設定ファイルをちょっと変更して service XXX restart、ぐらいなら誰にでもできますが、ansible-playbook -i prod.ini all.yml と入力して Enter するのはなかなか誰にでもできる作業ではありません(心理的な意味で)。


スライドにはありませんが、role の中の files/ について。

例えば php.ini を置く場合、当初はサーバでのパスと同じようにディレクトリを掘っていました。

roles/
  php/
    files/
      etc/
        php.ini

が、ディレクトリ階層が無駄に深すぎじゃない? と思ったので、次のようにフラットにしました。

roles/
  php/
    files/
        php.ini

問題は、このディレクトリ以外のファイルも置きたい場合です。例えば php のエラーログをローテートするための logrotate の設定を置きたい場合、最初のサーバのパスそのままの構成であれば次のようになりますが、

roles/
  php/
    files/
      etc/
        php.ini
        logrotate.d/
          php

フラットにすると次のようになり、わけがわからなくなります。

roles/
  php/
    files/
        php.ini
        php

ローカルのファイル名とサーバのファイル名が一致している必要はないので、次のようにしてみました。

roles/
  php/
    files/
        php.ini
        logrotate

さらに /etc/php.d/ にもファイルをコピーしたい場合、流石にこいつらまでフラットに置くのはつらいので、次のようにしています。

roles/
  php/
    files/
        php.ini
        logrotate
        php.d/
          00-aaa.ini
          00-bbb.ini
          00-ccc.ini

この構成なら php.d/ に with_fileglob も使えますし(ただ、with_fileglob は滅多に使いません)。


前述の通り、with_fileglob は滅多に使いません。なぜなら・・・

ローカルのファイルを削除しただけでサーバのファイルも削除されることを期待してしまう

からです。

synchronize を使う案も考えましたが・・・

php.d/ には自分で管理しないファイル(yum install で配置されたものを放置)もあり、あとで php 拡張モジュールを追加したときに synchronize で消されてしまう事故があったので、使うのをやめました。

with_fileglob の対象となっているディレクトリからファイルを削除した場合、

なんらかの方法でサーバからそのファイルを削除しなければならない

ので、それなら with_items で1つ1つ指定しておいて、ファイルを削除したときには task の方も修正しなければならないようにしておいた方が事故りにくいです(task を修正しなければプレイブックの実行時にコケるので)。

PhpStorm の PHPSTORM_META でサービスロケーターとかを入力補完

先日 Twitter を眺めていたら、PhpStorm Advanced Metadata というものを知ったので使ってみました。

下記はいわゆる社内勉強会的ななにかで話したときのスライドです。

スライドの通りですが、.phpstorm.meta.php という名前で次のようなファイルを用意しておくと、メソッドの引数に指定した文字列リテラルを元に戻り値の型が認識されるようになります。

<?php
namespace PHPSTORM_META {
    /** @noinspection PhpUnusedLocalVariableInspection */
    /** @noinspection PhpIllegalArrayKeyTypeInspection */
    /** @noinspection PhpDynamicAsStaticMethodCallInspection */
    $STATIC_METHOD_TYPES = array(
        \SL::get('') => array(
            'ore' instanceof \Ore,
            'are' instanceof \Are,
        ),
    );
}

そのため、次のようなコードでも入力補完が効きます。

<?php
class SL
{
    /**
     * @param string $name
     * @return mixed
     */
    public function get($name) {}
}

class Ore
{
    public function this_is_ore() {}
}

class Are
{
    public function this_is_are() {}
}

$o = new SL();
$o->get('ore')->this_is_ore();
$o->get('are')->this_is_are();

サービスロケーターを使っていると、オブジェクトのインスタンスをサービスロケーターから取得するたびにいちいち /* @var $ore Ore */ のように書く必要がありましたが、.phpstorm.meta.php をうまいこと作成するようにしておけばそんな必要はなくなります。

EC2 で CentOS 6 の HVM の AMI をゼロから作る

最近 AWS Marketprice で CentOS 7 の HVM の AMI が使えるようになりましたが、CentOS 6 の HVM の AMI は AWS Marketprice にはまだありません。

CentOS 6 でも t2.micro インスタンスが使いたかったので、ゼロから AMI を作成してみました。

概ね参考記事の手順そのままですが、極力最小の手順にしています。

参考記事

作業用インスタンスとボリュームの作成

Management console で次の手順で作業用のインスタンスと EBS を作成します。

作業用インスタンスでディスクイメージの作成

作業用インスタンスssh でログインし、アタッチした EBS に CentOS 6.5 x64 のディスクイメージを作成します。

フォーマットとマウント

messages の最後の方にアタッチされた EBS のデバイス名があるので確認します。

tail /var/log/messages
Oct 25 12:11:57 ip-xx-xx-xx-xx kernel: blkfront: xvdj: barriers disabled
Oct 25 12:11:57 ip-xx-xx-xx-xx kernel: xvdj: unknown partition table

わからなければ fdisk -l で調べてください。

パーティションを作成します。parted は使い方を知らないので fdisk を使います。

fdisk /dev/xvdj

フォーマットします。

mkfs.ext4 /dev/xvdj1

パーティションにラベルを付けます。下記の手順では / というラベルを付けています。

e2label /dev/xvdj1 /

マウントします。

mount /dev/xvdj1 /mnt
cd /mnt

CentOS 6.5

ディスクのマウント先である /mnt に yumCentOS 6.5 をインストールします。

作業用のインスタンスCentOS 6.5 なので yum の設定ファイルを作る必要はありません(作業用インスタンスの設定がそのまま使えます)。

acpid を入れているのは、入れておかないと Management console から Stop や Reboot したときに正常なシャットダウンやリブートが行なわれないような気がするからです(KVM はそうだった気がする)。

yum -y --installroot=/mnt --releasever=6.5 install @core kernel grub acpid

grub

grub.conf を作成します。console=ttyS0 を追加しておかないと Management console の Get System Log でブートのログが表示されません。

cat <<EOS> boot/grub/grub.conf
default=0
timeout=0
hiddenmenu
title CentOS 6.5 x86_64
        root (hd0,0)
        kernel /boot/vmlinuz-$(rpm --root=/mnt -q --queryformat "%{version}-%{release}.%{arch}" kernel) ro root=LABEL=/ console=ttyS0
        initrd /boot/initramfs-$(rpm --root=/mnt -q --queryformat "%{version}-%{release}.%{arch}" kernel).img
EOS

menu.lst のシンボリックリンクを作成します。

ln -s grub.conf boot/grub/menu.lst

grub をインストールします。まずは grub の stage ファイルをコピーします。

cp -a usr/share/grub/x86_64-redhat/* boot/grub/

作業用インスタンスの /dev を /mnt/dev にマウントします。

mount --bind /dev /mnt/dev

/mnt に chroot して grub をインストールします。

cat <<EOS | chroot /mnt grub --batch
device (hd0) /dev/xvdj
root (hd0,0)
setup (hd0)
EOS

/mnt/dev をアンマウントします。

umount /mnt/dev

SELinux

SELinux を無効にします。

sed -i '/^SELINUX=/c SELINUX=disabled' etc/selinux/config

IPv6

IPv6 を無効にします。

cat <<EOS>> etc/sysctl.conf

# ipv6 disable
net.ipv6.conf.all.disable_ipv6 = 1
net.ipv6.conf.default.disable_ipv6 = 1
EOS

fstab

fstab を作成します。

cat <<EOS>> etc/fstab
LABEL=/     /           ext4    defaults        1 1
devpts      /dev/pts    devpts  gid=5,mode=620  0 0
tmpfs       /dev/shm    tmpfs   defaults        0 0
proc        /proc       proc    defaults        0 0
sysfs       /sys        sysfs   defaults        0 0
EOS

いろいろコピー

作業用のインスタンスからいろいろコピーします。

\cp -a /etc/sysconfig/network-scripts/ifcfg-eth0  etc/sysconfig/network-scripts/ifcfg-eth0
\cp -a /etc/sysconfig/network                     etc/sysconfig/network
\cp -a /etc/rc.d/rc.local                         etc/rc.d/rc.local
\cp -a /etc/ssh/sshd_config                       etc/ssh/sshd_config

アンマウント

ディスクイメージの作成は終わったので /mnt をアンマウントします。

cd
umount /mnt

AMI 作成

Management console で次の手順で AMI を作成します。

  • EBS ボリュームをインスタンスからデタッチする
  • EBS ボリュームからスナップショットを作成する
  • スナップショットから AMI を作成する
    • 以下の箇所に注意して作成します
      • Architecture: x86_64
      • Virtualization type: Hardware-assisted virtualization
      • Root device name: /dev/sda1
      • Kernel ID: Use default
      • RAM disk ID : Use default

AMI からインスタンスを作成

作成した AMI から t2.micro インスタンスを作成し、普通に起動すれば成功です。

なぞ?

AWS で実際に作業する前に、手順の確認のために同じような方法で VirtualBox で既存のゲストにアタッチしたディスクにブート可能な CentOS 6.5 のディスクイメージを作ってみたのですが、なぜか初回起動時に SELinux が自動的に enforcing になりました。

しかも、ssh はもとよりコンソールからもログインできません(ログインすると直後に切断される)。

シングルユーザーモードでブートして /etc/selinux/config を見てみると enforcing になっていました。もちろんディスクイメージの作成時には /etc/selinux/config を disabled に書き換えています。タイムスタンプを見てみると、ブート時の時刻になっています。

ディスクイメージに作成時に selinux-policy と selinux-policy-targeted を削除しておいたり、あるいは、カーネルパラメータで enforcing=0 と指定しておいたりすれば大丈夫ですが、VirtualBox で初回起動時に SELinux が必ず有効になるような機構でもあるのでしょうか。。。?

Eclipse PDT + MakeGood でも SSH 経由で PHPUnit を実行する

PhpStorm 8 が正式にリリースされましたね。

PHP Remote Interpreters というものが実装されたので PHPUnitSSH 経由で実行するのがとても簡単になりました。

が、少し触ってみたところちょっと微妙なことろもありました。下記の記事にも記載されていますが Test scope に Directory や Class や Method を指定するとローカルとリモートのパスの違いでエラーになります。

この記事ではシンボリックリンクでどうにかする方法が紹介されていますが、ローカルが Windows でリモートが Linux な環境で開発している私のようなキチガイにそれは難しいです。なんせ D:\path\to\hoge みたいなパスですから。

わりと Ctrl+Shift+F10 でクラスやメソッド指定でテストを実行することがあるので、この機能が使えないと困ります。なので少し前に書いた下記の方法をしばらくは使っていこうと思ってます。

なお、上記の記事に埋め込んでいる Gist のコードは少し古いです。コードは こっち に移しました。使い方とかは多分変わっていませんが、まぁ自分しか使わない俺得なものなので細かい説明はなくてもいいでしょう。

↓追記 2014/09/19

なわけないですね、キチンと動作します。

ローカルとリモートのパスのマッピングのための Deployment の設定が必要なのですが、私は cifs でローカル~リモートを共有しているので Deployment は設定していませんでした。 Deployment でマッピングを設定するとうまく動作しました。ただ cifs で共有している状態で本当に PhpStorm でデプロイしてしまうと、コピー元とコピー先が同じファイルであるがゆえにファイルの中身が消し飛んでしまったりするのでできれば設定したくなかったです。

↑追記 2014/09/19

.

.

.

それはそれとして、自社内では Eclipse PDT の方が主流っぽいので Eclipse PDT + MakeGood でも同じようなことをやってみました。

私自身もう Eclipse PDT は使っていないので本当に誰得ですが、せっかくなので記事にしました。

Eclipse PDT + MakeGood での設定

phpunit と stagehand-testrunner を composer でインストールします。

stagehand-testrunner は MakeGood から phpunit を実行するときのランチャーのようなもので MakeGood をインストールすると一緒に含まれていますが、リモートにも必要なので composer でインストールします。

忘れがちなのが stagehand-testrunner を composer でインストールした後は vendor/bin/testrunner compile する必要があります。

次に MakeGood をそれっぽく設定します。

まずは プリロードスクリプト に後述する remote-makegood.php を指定します。また、PHPUnit タブの XML設定ファイルphpunit.xml が指定できるようになっていますが、ここでは指定しないでください。phpunit.xml は後述の remote-makegood.local.php で指定します。その他は普通に設定してください。

次に remote-makegood.php と同じディレクトリに remote-makegood.local.php を作成します。

<?php
// SSH のログインユーザー
$remote_user = 'ore';

// リモートホスト名
$remote_host = 'ore-no-server';

// リモートのパス
$remote_dir = '/home/ore/work';

// ローカルのパス
$local_dir = dirname(__DIR__);

// プリロードスクリプトのパス
$preload_script = 'vendor/autoload.php';

// phpunit.xml のパス
$phpunit_config = 'phpunit.xml';

$remote_user$remote_host$remote_dir$local_dir は見ての通りのものです。

$preload_script は MakeGood の設定でプリロードスクリプトに指定するはずだったものです。プリロードスクリプトには remote-makegood.php を指定する必要があるので、本来指定するはずだったものをここで指定します。普通は vendor/autoload.php のような PHPUnit をオートロードするためのものが指定されると思います(後述の通りこの方法なら vendor/autoload.php をプリロードする必要はありませんが)。

$phpunit_config には phpunit.xml のパスを指定します。私の実装がクソいので 諸事情により MakeGood の設定で phpunit.xml を指定するとうまく動作しなくなります。その代わりに remote-makegood.local.phpphpunit.xml を指定してください。なお、キチンと実装すれば MakeGood の設定で指定した phpunit.xml をそのまま活かすこともできるはずです。

なお、$preload_script$phpunit_config も、$local_dir からの相対パスで指定します。

正しく設定されていれば MakeGood でテストを実行したときに ssh 経由でテストが実行されて結果が IDE 上に表示されます。

なお、デバッグ実行には対応していません。これは実装が手抜きだからであって、頑張れば不可能ではないと思います。

remote-makegood.php のざっくり解説

PhpStorm でやったときと比べるとかなり手抜きな実装になっています。私が常用するものではないので。。。

前述の通り MakeGood は phpunit を直接実行するのではなく stagehand-testrunner というランチャーを利用して phpunit を実行します。ちなみに次のような $argv が渡されます(設定とか実行時の状況によって引数は増えたり減ったりします)。

Array
(
    [0] => D:\app\eclipse-php-luna-R-win32\plugins\com.piece_framework.makegood.stagehandtestrunner_3.1.1.v201409021510\resources\php\bin\testrunner.php
    [1] => --no-ansi
    [2] => phpunit
    [3] => -p
    [4] => D:/ore/devel/phpunit-via-ssh-on-ide/tests/remote-makegood.php
    [5] => --log-junit=C:\Users\ore\AppData\Local\Temp\com.piece_framework.makegood.launch\MakeGood1410358831955.xml
    [6] => --log-junit-realtime
    [7] => -R
    [8] => --test-file-pattern=Test(?:Case)?\.php$
    [9] => D:/ore/devel/phpunit-via-ssh-on-ide/tests
)

テストコードのパス $argv[9] はローカルのパスからリモートのパスに書き換えるだけです。

プリロード $argv[4]vendor/autoload.php とかに書き換えます。stagehand-testrunner を composer からインストールしていれば vendor/autoload.php は勝手に読まれるので、実はプリロードで vendor/autoload.php を読む必要はありません。

$argv[5]--log-junit は少し厄介です。

MakeGood はこのファイルを監視していて、このファイルに出力された内容を元に IDE にテスト結果を表示しているようです。

PhpStorm で SSH 経由で PHPUnit を実行させたときはリモートの標準出力(正確には PHP の出力、いわゆる php://output)をローカルの標準出力に書き込むだけで良かったのですが、MakeGood の場合はリモートの標準出力と標準エラーと --log-junit の合計で3つのストリームをどうにかする必要があります。

ssh 経由なので標準出力と標準エラーで2本のストリームが使えます。なので、リモートの標準出力と標準エラーはローカルの標準出力へ、--log-junit はローカルの標準エラーを経由して本来の出力先にリダイレクトすることにします。

また、--log-junitIDEスタックトレースなどをローカルのパスで表示するためにパスの書き換えも行います。

まず、リモートの標準出力と標準エラーをローカルの標準出力に流す方法ですが、proc_open$desc で次のように指定しました。

$desc = array(
    0 => array('file', '/dev/null', 'r'),
    1 => array('file', 'php://stdout', 'w'),
    2 => array('file', 'php://stdout', 'w'),
);

$proc = proc_open($cmd, $desc, $pipes);

--log-junit への書き込みを標準エラーに流す方法は、簡単な方法が思いつかなかったので FIFO(名前付きパイプ)を使いました。

$tmp = tempnam(sys_get_temp_dir(), "makegood-junit-");
unlink($tmp);
posix_mkfifo($tmp, 0600);

try {
     :

    $fifo = fopen($tmp, 'r');
    $stderr = fopen('php://stderr', 'w');

    while (strlen($data = fread($fifo, 1024)) !== 0) {
        $data = str_replace($remote_dir, $local_dir, $data);
        fwrite($stderr, $data);
    }

     :
} finally {
    unlink($tmp);
}

remote-makegood.php

remote-makegood.php の全文は次のとおりです。