オープンソースこねこね

Webプログラミングなどについてあれこれ。

PHPでcronのように定期実行をするプログラムを書いた

PHP

kohkimakimoto/workerphp · GitHub

自分で使う分には最低限、必要な機能を実装できたので紹介します。以下のようなPHPファイルを記述してPHPコマンドラインから実行するとプロセスが起動しっぱなしになりcront_timeで指定したスケジュールでジョブを定期実行してくれます。

<?php
require_once __DIR__.'/vendor/autoload.php';

$worker = new \Kohkimakimoto\Worker\Worker();

// job for every minute.
$worker->job("hello", ['cron_time' => '* * * * *', 'command' => function(){
    echo "Hello world\n";
}]);

// job runs a shell command.
$worker->job("uptime", ['cron_time' => '10 * * * *', 'command' => "uptime"]);

$worker->start();

ジョブはクロージャPHPのコードとして書くか、シェルのコマンドを文字列で直接指定することができます。インストールはcomposer installするだけですが、その他詳しい情報はリポジトリのREADMEを参照してください。内部的にジョブはプロセスをforkして実行させているので、PHPpcntlエクステンションがインストールされている必要があります。linuxならyumとかでインストールできると思います。

cronが普通に使える環境ならまあそれを使えばいいんですが、個人の用途でherokuの無料枠dynoを使って定期実行処理をしたかったので、こんなの作りました。

Web API

cronにない拡張機能として、簡易なWeb APIを提供するHTTPサーバを組み込んでいます。

$worker->httpServer->listen();

のように書くと、8080ポートを開きます。ポートを変更したいときは

$worker->httpServer->listen(8888, 'localhost');

のようにもかけます。HTTPサーバが立ち上がったら、

$ curl -XGET http://localhost:8080/{ジョブ名}

でジョブの情報を取得

$ curl -XPOST http://localhost:8080/{ジョブ名}

でジョブを実行することができます。また$worker->httpServer->setAPIKey()を使えば、文字列一致だけで判定する簡易な認証機能をつけられます。 ちなみに、私は以下の様な設定でherokuのPHPスタック上で動かしています。

<?php
require_once __DIR__.'/vendor/autoload.php';
date_default_timezone_set('Asia/Tokyo');

use Kohkimakimoto\Worker\Worker;

$worker = new Worker();
// herokuはhttpのportを動的に割り当てるので環境変数から取得する
$worker->httpServer->listen(getenv('PORT'));
// apiにアクセスするときのキー
$worker->httpServer->setAPIKey('vjdioaewdg49q3tg...');
// 5分毎にメモリ使用量などをログに出力する設定
$worker->stats->on();

$worker->job("hoge", ['cron_time' => '* * * * *', 'max_processes' => 1, 'command' => function(){
    // ...
}]);

$worker->job("foo", ['cron_time' => '* * * * *', 'max_processes' => 1, 'command' => function(){
    // ...
}]);

// 30分に一回dynoをwebからアクセスしてスリープさせないようにする。
$worker->job("wakeup_dyno", ['cron_time' => '*/30 * * * *', 'max_processes' => 1, 'command' => function(){
    file_get_contents("https://my_worker_xxx.herokuapp.com/?apiKey=vjdioaewdg49q3tg...");
}]);

$worker->start();

とりあえず、1日動かしっぱなしにしてみたが問題はなさそうです。

UIPageViewControllerをUINavigationControllerにいれたらナビゲーションバー部分に潜り込まなくなった件

ios

iOS8でUIPageViewControllerトランジションスタイルをUIPageViewControllerTransitionStyleScrollにしたときのみ発生する。。。マジなんなのこれ。。。しかも一度でも画面をタップすると、潜り込む(正常な)位置に移動するという。挙動が奇妙すぎてStack Overflowでも見つからないし。iOS8のバグを疑ってしまう。ヾ(`Д´)ノ"

f:id:kohkimakimoto:20141203064908g:plain

このサンプルはXcodePage-Based Applicationが作った雛形のプロジェクトを元に、RootViewControllerをNavigationControllerにいれる、のとUIPageViewControllerの初期化処理を以下のようにUIPageViewControllerTransitionStyleScrollを指定するように変えたもの。

self.pageViewController = [[UIPageViewController alloc] initWithTransitionStyle:UIPageViewControllerTransitionStyleScroll navigationOrientation:UIPageViewControllerNavigationOrientationHorizontal options:nil];

表示されるViewのframeなどを調べてみてもx=0,y=0となっていて変なところはない。それでViewの階層を見てみたらこんな感じになっている。 なんか下にズレてる。。。

f:id:kohkimakimoto:20141203065549p:plain

_UIQueuingScrollViewというViewの位置がy=-64.0という謎な状態になっていて、その子となるViewがその分だけ下にずれている。 しかもこれviewWillAppear:などのフェーズでは確認できず、viewDidAppear:まで到達して初めてこの状態になる。

というわけで以下のようなワークアラウンドを入れた。

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    
    if (([[[UIDevice currentDevice] systemVersion] floatValue] >= 8.0)) {
        for (UIView *v in self.pageViewController.view.subviews){
            if ([v isKindOfClass:[UIScrollView class]]) {
                UIScrollView *scv = (UIScrollView *)v;
                CGPoint offset = scv.contentOffset;
                if (offset.y < 0) {
                    offset.y = 0;
                    scv.contentOffset = offset;
                }
            }
        }
    }
}

iOS8の場合、UIPageViewControllerUIScrollViewをとってきてマイナス値なら0に書き換えるということをやっています。これでなんとか対応できた。

追記

コメント欄でアドバイスをいただいて「UIPageViewControllerのAdjust Scroll View Insetsのチェックを外す」で対応できることがわかりました。

Goの開発環境

最近Go言語をいじっているので、現時点での開発環境についてまとめておく。ツールを先に列挙しておくと以下のものを使っている。

エディタ

Sublime Text3を使っている。普段PHPを書くのに使っていて慣れているからであって最適だとは思ってはいない(静的型言語はIDEを使う方がいいとは思っているのだが、ショートカットキーを新たに覚えたりするのがつらいので使い続けている)。

これにGo用のプラグインGo​Sublimeをインストールしている。

また関数定義などにジャンプするためにctagsも使っている。ctags自体はhomebrewでインストールして、SublimeにはCtagsプラグインを入れている。標準だとctagsはGoに対応していないのだが

go - ctag database for golang - Stack Overflow

にあるように、~/.ctags

--langdef=Go
--langmap=Go:.go
--regex-Go=/func([ \t]+\([^)]+\))?[ \t]+([a-zA-Z0-9_]+)/\2/d,func/
--regex-Go=/var[ \t]+([a-zA-Z_][a-zA-Z0-9_]+)/\1/d,var/
--regex-Go=/type[ \t]+([a-zA-Z_][a-zA-Z0-9_]+)/\1/d,type/

と書き込んでおけば、関数定義元に(ある程度)ジャンプできるようなる。完璧にとはいかないが、今のところはこれでいいかと。

GOPATHとディレクトリ構造

ghqを使ったローカルリポジトリの統一的・効率的な管理について - delirious thoughts

基本は上記の記事の通り。GOPATH=$HOMEに設定してghqでGo以外のリポジトリも含めて~/src配下で一括管理する。

ただ、これだとctagsでタグを生成する際に全てのリポジトリを対象にしなくてはならず、タグ生成処理がものすごく重くなってしまい使いものにならなかった。やはり依存パッケージは個々のリポジトリ配下にほしい。

いろいろ試行錯誤した後、次のようにdirenvを使う方法に落ちついた。direnvはカレントディレクトリごとに環境変数を自動で切り替えてくれるツールで、以下の記事が参考になる。

direnvを使おう - Qiita

まず

brew install direnv

direnvをインストールし、zshの場合は~/.zshrcに以下を記述しておく。

export EDITOR=vim
eval "$(direnv hook zsh)"

さて、ホームのsrcディレクトリは以下のようになっている。

src
├── github.com
│   ├── laravel
│   │   ├── framework
│   │   └── laravel
│   ├── kohkimakimoto
│   │   └── altax
│   ├── php
│   │   └── php-src
│   └── piranha
│       └── gostatic

ここでGo言語の場合はリポジトリのルートに.envrcを作成して

export GOPATH=$(pwd)/_vendor/:$GOPATH

と書いておく。これでGo言語のリポジトリに移動した時のみGOPATHがそのリポジトリ配下の_vendorディレクトリにも設定される。go getで外部パッケージをインストールするとき_vendoer配下にインストールされるようになるし、go build時もここのパッケージを参照するようになる。ctagsのタグ生成もリポジトリルートで行えば依存のあるパッケージをだけを対象にタグを生成できる。

参考にしたサイトは以下。

ghqによるGoのプロジェクト管理 & プロジェクト毎で外部モジュール管理 - Qiita

これからGoを始める人のためのTips集 #golang - The Wacul Blog

Gomで依存パッケージを一括で_vendorにダウンロード

_vendorに依存パッケージを配置できるようになったのはいいのだが、依存物をいちいち手作業でgo getするのは非効率なので一括でダウンロードできるようにGo言語版のBundlerなやつであるGomを使っている。

mattn/gom · GitHub

以下のコマンドを叩くとgo buildを実行してインストールされていないパッケージをエラーメッセージから抽出してGomfileに吐き出す。

go build 2>&1 | awk '/cannot find package/{print "gom " $5}' >> Gomfile

これでGomfileができたら

gom install

として全ての依存パッケージを_vendor配下にダウンロードする。これでGoのプロジェクトがビルドできる\(^o^)/

xcode6でViewのconstraintに-16がついてiOS7.1でレイアウトがずれる件

ios

xcode6でビューを重ねている状態で、子のビューを親のビューの端にぴったりあわせるるために、オートレイアウトを指定するとなぜかHorizontal Spaceに-16が設定される。しかもこれをiOS8で動かすと綺麗に端がぴったりあって表示されるのに、iOS7.1で動かすとずれて表示されるという状態に。

これがiOS8で

f:id:kohkimakimoto:20141113120025p:plain

こっちがiOS7.1

f:id:kohkimakimoto:20141113120030p:plain

青い領域が子のViewでconstraintが指定してある。青いViewの中にあるテキストフィールドがずれてしまっているが判るだろう。ついでに画面上にピッタリつけたはずの部分にも隙間ができてしまっている。。。

そしていつものstackoverflowから解決策を。

ios - How do I get rid of the -16 when doing horizontal layout to the edge of the superview in Xcode 6? - Stack Overflow

Constraintの指定からRelative to marginのチェックを外す。これが付いているとConstraintの対象が親のVIewの端にならないためなんだそうだ。しかしiOS7と8でUIレンダリング(オートレイアウト周り?)の挙動が違うのは辛いものがあるなあ(T . T)。。。

JenkinsとDockerでTravisっぽいCIサーバを育ててみている

最近プライベートなプロジェクトのCIにはcircleciとかが人気なんでしょうかね。

GitHub 時代のデプロイ戦略 - naoyaのはてなダイアリー

近頃のCIサーバはアプリケーションのテストだけじゃなく、インフラのテストやデプロイ、ChatOpsなどgitやチャットツールなど他のシステムと連携した自動化のための必須プラットフォームといった感じになってきてる。とはいえ、趣味で開発しているプロダクトに余計なコストはかけたくない。ああ、でもやっぱCIはしたい。

そんなわけで以前から契約だけしていて放置気味だった、さくらのVPSの1GにjenkinsをたててオレオレCIを育てているのでその辺のことを書いてみる。CIの実行環境はDockerを使って仮想化し、ジョブの内容はTravisやcircleciのようにリポジトリ側のyamlファイルに記述できるようにしてみた。構成をざっくりと図解すると以下のようになる。

f:id:kohkimakimoto:20141013212556p:plain

また参考にさせていただいたのは以下の記事。

Docker + Jenkins + travis.yml parser 作って Travis っぽいものを作った話 - from scratch

Use Docker + Jenkins to run GitHub tests

jenkinsでのunitテストは、dockerでクリーン環境を作って行う!!shinofara's Blog (*´ω`*) | shinofara's Blog (*´ω`*)

ベースとなっている環境

CentOS6.5の上に構築している。さくらのVPSのデフォルトがそうなのと、個人的に使い慣れているというのが最大の理由。ただCentOSのDockerはカーネル周りのバグでディスク領域が開放されないことがあるらしいので、今後移行するかもしれない。

Docker on CentOS 6.5 で詰んだのでメモ - sonots:blog

CentOS6系のカーネルに上記のバグフィックスがバックポートされるのも期待している。。。まあ今のところ、仕事でつかっているわけではなく、ディスク領域にも余裕があるのと環境構築はchefでなるだけ自動化しながら作っているので、ディスクが詰まったら最悪、環境を再インストールすればいいかなという判断でやっている。

Dockerのインストール

Dockerのインストール自体について特記することはあまりない。epelリポジトリを使えるようにしておいて以下の様なchefのレシピを書いた。実際にやってることはyum install docker-io/etc/init.d/docker startと同じだ。

docker/recipes/default.rb

package "docker-io" do
  action :install
end

service "docker" do
  action [:enable, :start]
end

jenkinsのインストール

CentOS上のjenkinsのインストールについては以前に記事を書いた。今回はそれをちょこちょこ修正した(yumリポジトリからインストールするなど)。詳細は以下にペーストしたレシピの内容を見てもらえばいいが、jenkinsからdockerを実行するために jenkinsユーザのuidを明示的(uid:45678)に指定したりdockerグループの追加したりしている。Dockerコンテナ内でジョブを実行するユーザとjenkinsの実行ユーザのIDは同じにしておかないといろいろパーミッション周りでハマる。

jenkins/recipes/default.rb

group "jenkins" do
  gid    45678
  action [:create, :manage]
end

user 'jenkins' do
  comment  'Jenkins Continuous Build server'
  uid      45678
  group    'jenkins'
  home     '/var/lib/jenkins'
  shell    '/bin/false'
  password nil
  action   [:create, :manage]
end

script "install_jenkins_yum_repo" do
  interpreter "bash"
  user "root"
  cwd "/tmp"
  code <<-EOH

  wget -O /etc/yum.repos.d/jenkins.repo http://pkg.jenkins-ci.org/redhat/jenkins.repo
  rpm --import http://pkg.jenkins-ci.org/redhat/jenkins-ci.org.key

  EOH

  not_if "test -e /etc/yum.repos.d/jenkins.repo"
end

package "jenkins" do
  action :install
end

service "jenkins" do
  action [:enable, :start]
end

group "docker" do
  action [:modify]
  members ["jenkins"]
  append true
end

jenkins cliプラグインのインストール

jenkinsのプラグインもchefでインストールさせるため以下のようなレシピを書いた。jenkinsは起動までに時間がかかるのでscript[install-jenkins-cli]jenkins-cli.jarをダウンロードする際に10秒待ち、HTTPレスポンス503はリトライする仕組みにしている。

jenkins/attributes/default.rb

default['jenkins']['cli_url'] = "http://127.0.0.1:8080/jnlpJars/jenkins-cli.jar"
default['jenkins']['jenkins_url'] = "http://127.0.0.1:8080/"
default['jenkins']['wait_for_boot'] = "10"

default['jenkins']['plugins'] = [
  "git",
  "cloverphp",
  "simple-theme-plugin",
  "jquery",
  "gravatar",
  "disk-usage",
  "envinject",
  "extra-columns",
  "categorized-view",
  "ci-skip",
  "timestamper",
  "monitoring",
  "view-job-filters",
  "locale",
  "sidebar-link",
  "pegdown-formatter",
  "ansicolor"
]

jenkins/templates/default/jenkins.rb

#!/usr/bin/env bash

java -jar /usr/lib/jenkins/jenkins-cli.jar -s <%=node['jenkins']['jenkins_url']%> "$@"

jenkins/recipes/default.rb

template "/usr/local/bin/jenkins" do
  source "jenkins.erb"
  owner "root"
  group "root"
  mode "0755"
end

cli_url = node['jenkins']['cli_url']
wait_for_boot = node['jenkins']['wait_for_boot']

script "install-jenkins-cli" do
  interpreter "bash"
  user "root"
  cwd "/tmp"
  code <<-EOH
    sleep #{wait_for_boot}

    http_response_code=503
    while [ $http_response_code -eq 503 ]
    do
      http_response_code=`curl -LI #{cli_url} -o /dev/null -w '%{http_code}' -s`
      sleep 5
    done

    wget -t 5 --waitretry 5 -O /usr/lib/jenkins/jenkins-cli.jar #{cli_url}
  EOH
  not_if "test -e /usr/lib/jenkins/jenkins-cli.jar"
end

directory "/var/lib/jenkins/updates" do
   owner "jenkins"
   group "jenkins"
   mode "0755"
   action :create
end

# https://issues.jenkins-ci.org/browse/JENKINS-10061
# https://gist.github.com/rowan-m/1026918
script "update-jenkins-updatecenter" do
  interpreter "bash"
  user "jenkins"
  cwd "/tmp"
  code <<-EOH
    curl -L http://updates.jenkins-ci.org/update-center.json | sed '1d;$d' > /var/lib/jenkins/updates/default.json
  EOH
  not_if "test -e /var/lib/jenkins/updates/default.json"
end

node['jenkins']['plugins'].each do |plugin_name|

  execute "install-jenkins-plugin-" + plugin_name do
    user "root"
    command "/usr/local/bin/jenkins install-plugin " + plugin_name
    action :run
    not_if "/usr/local/bin/jenkins list-plugins | awk '{print $1}' | grep ^#{plugin_name}$"
    notifies :run, "execute[jenkins-safe-restart]"
  end

end

execute "jenkins-safe-restart" do
    command "/usr/local/bin/jenkins safe-restart"
    action :nothing
end

yamlパーサとコンテナの起動スクリプト

CIのジョブはTravisのようにリポジトリ側のyamlで制御、設定できるようにした。yamlファイルはこんな感じ。

.jenkins.yml

container:
    image: jenkins-ci-base

before_script:
    - composer install --dev --no-interaction

script:
    - php vendor/bin/phpunit -c phpunit-ci.xml.dist

これをパースしてimageで指定されたDockerイメージをrunする。この辺の処理は使い慣れたPHPで実装した。ソースは公開していないが、具体的には以下のようなことをやっている。

  • リポジトリのルートにある.jenkins.ymlをパース。
  • before_scriptscriptで指定された内容からそれぞれbefore_script.shscript.shのようなスクリプトファイルを生成してJenkinsのワークスペースに出力する。
  • 出力したスクリプトファイルを順に起動するstart.shをJenkinsのワークスペースに出力する。
  • imageで指定されたdockerイメージにワークスペースをマウントしてコンテナを起動。具体的にはdocker run -v $WORKSPACE:/home/worker/workspace -w /home/worker/workspace -u worker $IMAGE /bin/bash -l start.shのようなコマンドを実行する。
  • dockerが処理を終えたあとdocker rmを実行してコンテナを削除する。

この一連の処理を行うPHPスクリプトをjenkinsのジョブ設定の「ビルド」->「シェルの実行」で起動するように設定しておく。

Dockerイメージ

Dockerイメージはあらかじめ作っておくのだが、jenkinsから起動するために以下の決められた仕様で構成している

  • uid45678workerユーザがいる(ホスト側のjenkinsユーザと同じuid)
  • /home/worker/workspaceディレクトリがある(ホスト側のjenkinsワークスペースがマウントされる)
  • workerはパスワードなしでsudoできる

この仕様を満たす基本的なDockerイメージを作成するDockerfileは以下のようになる

FROM centos:centos6

# basic settings
RUN rpm -ivh http://dl.fedoraproject.org/pub/epel/6/x86_64/epel-release-6-8.noarch.rpm
RUN rpm -ivh http://rpms.famillecollet.com/enterprise/remi-release-6.rpm

RUN yum -y groupinstall "Base" "Development tools"
RUN yum -y install --enablerepo=remi,epel \
  sudo \
  readline \
  readline-devel \
  compat-readline5 \
  libxml2-devel \
  libxslt-devel \
  libyaml-devel \
  git \
  make \
  autoconf \
  automake \
  bison \
  libtool \
  sysstat \
  gettext \
  traceroute \
  openssl \
  openssl-devel \
  curl \
  wget

# add worker user
RUN useradd -u 45678 -d /home/worker -m -s /bin/bash worker && \
  mkdir /home/worker/workspace && \
  chown worker:worker /home/worker/workspace && \
  echo "worker    ALL=(ALL)    NOPASSWD: ALL" > /etc/sudoers.d/worker && \
  sed -i 's/.*requiretty$/#Defaults requiretty/' /etc/sudoers

# Enable to run sudo in the script
RUN sed -i 's/.*requiretty$/#Defaults requiretty/' /etc/sudoers

# timezone
RUN echo 'ZONE="Asia/Tokyo"' > /etc/sysconfig/clock && \
  rm -f /etc/localtime && \
  ln -fs /usr/share/zoneinfo/Asia/Tokyo /etc/localtime

#################################
# default behavior is to login by worker user
#################################
CMD ["su", "-", "worker"]

これで、dockerコンテナ内でworkerというユーザが処理を実行する。あとは必要な環境ごとにカスタマイズしたDockerイメージを用意しておけば、いろんな環境でCIができる。ちょうどTravisPHPRubyといった言語ごとのテスト環境を用意してくれるように、例えばPHPをインストールしたjenkins-ci-phpRubyをインストールしたjenkins-ci-rubyというDockerイメージを作っておいて、

container:
    image: jenkins-ci-php

container:
    image: jenkins-ci-ruby

などと.jenkins.ymlで指定すればいい。

まとめ

ここ数日運用してみての感想だが、これはかなりいい感じ。現在の自分の用途の範囲では、ほぼTravisでやれることがプライベートでも実現できている。 CIの実行環境はDockerによって独立し常に使い捨てにされるので、サーバ設定を丸ごと書き換えるようなプロビジョニングのテストなどにも使えると思う。

あとjenkinsのテーマをアトラシアン風にする djonsson/jenkins-atlassian-theme · GitHub があるので、これをちょっとカスタマイズして、UIの見た目も変えてみた。

f:id:kohkimakimoto:20141014055646p:plain

やっぱし、コンソール表示は黒背景がいい!

UITableViewのPlainスタイルで空のセルを表示させないようにする。

iOSアプリを作っていて普通にUITableViewを使っていると、表示するデータをもつセルが1,2行しかないとき、空のセルが画面の残りの部分を埋めてしまいます。

f:id:kohkimakimoto:20141010120109p:plain

この空のセルを表示させたくないときは、以下のようなコードを書けばよいです。

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    
    self.tableView.tableFooterView = [[UIView alloc] initWithFrame:CGRectZero];
}

このコードによってUITableViewが(高さ0の)フッターを表示させようとするため、明示的に指定したセル以外は表示させないようになるそうです。

f:id:kohkimakimoto:20141010120312p:plain

参考にした記事(いつもお世話になっておりますstackoverflow)

objective c - ios- uitableview displaying empty cells at the end - Stack Overflow

PHP5.5+OPcacheでシンボリックリンクでデプロイするとキャッシュが消えない

PHP

という問題にぶち当たりました。 どういうことかを話す前にPHPアプリケーションの個人的なデプロイ構成について説明します。 要はCapistranoのデプロイと同様なのですが、デプロイ先は以下のようなディレクトリ構成になっています。

.
|-- current -> /path/to/releases/20141011000001
|-- releases
|   |-- 20141011000000
|   `-- 20141011000001

currentアプリケーションサーバ(apachephp-fpm)から参照される箇所で、releases配下の最新のアプリケーションへのシンボリックリンクになっている。新しくアプリをデプロイするとreleases配下に20141011000002のようなタイムスタンプで新しくディレクトリが作成され、そこにアプリケーションのコードが配置される。その後currentシンボリックリンクが新しいアプリケーションへと切り替えられる。これによってダウンタイムなしでPHPアプリのコードをデプロイできます。

OPcacheのキャッシュ

ところが、PHP5.5+php-fpm+OPcacheという環境でこの構成のデプロイをためしたところ、実行中のアプリケーションにデプロイコードが反映されないという問題が生じました。 (※OPcacheはPHP5.5の新しいコードキャッシュシステムでPHP5.4以前のAPCに替わるものです)

調査していたら以下の解説記事を発見しました。

OpCache and symlink-based deployments

要約すると

  • OPcacheはAPCとはファイルのキャッシュの仕方が違う。
  • OPcacheはシンボリックを解決して、実ファイルパスの状態でキャッシュする。
  • よってシンボリックリンクを更新しても、実ファイルパスのキャッシュが保持されてしまう。

というわけだそうです。(このへんをちゃんとPHPのソース読んで、自分で裏付けとれるといいと思うのですが。。。技術力の低さが露呈しますね。。。(ー_ー;))

上記の解説記事では対応方法もいくつか提示されていて

  • webサーバの設定を直接書き換える(シンボリックリンクの代わりにweb(AP)サーバが参照するパスを直接書き換える)
  • OPcacheをリセットする
  • php-fpmを再起動する(多少のダウンタイムあり)

ansibleやpuppetを使っていればデプロイでwebサーバの設定を書き換えるのは簡単かも、と言及されています。 ただ、シンボリックリンクを使い続けたいならキャッシュをリセットするか、php-fpmを再起動する。 OPcacheはキャッシュをリセットする関数opcache_resetをもっているので、 これを使用した対応方法も(Laravelを使った例で)示されています。

さて、自分はどーしようか悩み中。 デプロイタスクがちょっとだけど複雑になるのと、コードキャッシュはアプリケーションより一段下の技術レイヤと考えているので、 それのリセット処理をアプリケーションのコードに直接入れるような対応はやりたくない。

この際、最近盛り上がってきているdockerでBlue-Green Deployment的なことをやってしまうのもアリかなと考えています。