投稿者「kanazawa」のアーカイブ

PHP8.1バージョンアップのコンテナOS選択

kanazawa|2022年07月22日
PHP

SREチーム、およびPHPバージョンアップチームの金澤です。

先日、ランサーズをEC2での運用からFargateベースのDocker運用に移行できたためPHP7.3→8.1のバージョンアップに着手しています。

※本番環境のコンテナ化については「Lancers本番環境のコンテナ化が完了しました」を参照。

バージョンアップがやや遅れていましたが、バージョンアップ前にDockerベースでの運用体制確立を優先していました。

Dockerベースの運用になったおかげで、CircleCI + によるローリングデプロイが確立し、GitHubのマージボタンを押したタイミングで、Dockerfileも含めた更新リリースが可能になり、バージョンアップのようなサーバー設定の変更リリースも非常にやりやすくなりました。(EC2 + ansible時代は、サーバーの設定関係はオートスケーリングを含め細かく面倒を見る必要があるため大変でした)

今回は、PHP8.1バージョンアップに伴う、OS選択の苦労話を書きたいと思います。

Amazon Linux2で構築

先日、EC2からコンテナに移行したときのPHPバージョンは7.3でした。先にコンテナ運用していた他サービスでは、Alpineベースで構築していたのですが、ランサーズではAmazon Linux2で構築しました。

ランサーズでは、EC2時代からAmazon Linux2を使っており、ログの加工、および転送を行う際にawslogsを使っていたこともあり、互換性を重視した選択でした。

PHP7.3はAmazon Linux2のyumでインストール可能でしたが、PHP8.1ではサポートしていません。
https://repost.aws/questions/QUsfhDbww4SAy8i5Jmd4vcMg/amazon-linux-2-amazon-linux-extras-php-8-1-support

そのため、remiでインストールしました。
※参考資料としてDockerfileを以下のGitHubリポジトリにアップしておきました。https://github.com/LancersDevTeam/PHP_versionup/tree/master/PHP7.3toPHP8.1/docker.amazonlinux2/prd/app-admin

結果、本番環境のawslogsも問題なく動作しました。順調にPHP8.1への移行が完了しました。
(と、このときは思っていました)

M1 Macで開発環境が動かない問題

本番環境のPHP8.1化も終了し、開発メンバーにも開発環境をPHP8.1に移行してもらうようにお願いしていましたが、開発メンバーの1人から、開発環境がビルドできないとの報告がありました。

原因の切り分けをした結果、M1 MacでPHP8.1版のコンテナがビルドできないことが判明。

remiリポジトリ( http://rpms.remirepo.net/enterprise/ )を調査してみると、remi7には、x86_64のパッケージしかなかったため、remi8を調べてみましたが、remi8にはaarch64自体存在しませんでした。

x86_64
remi7:http://rpms.remirepo.net/enterprise/7/remi/x86_64/repoview/
remi8:http://rpms.remirepo.net/enterprise/8/remi/x86_64/repoview/

aarch64(remi7にphp8がない。remi8はそもそも存在しない)
remi7:
http://rpms.remirepo.net/enterprise/7/remi/aarch64/repoview/
remi8:http://rpms.remirepo.net/enterprise/8/remi/aarch64/repoview/ (Not Found)

remi自体、個人で運営しているリポジトリで、aarch64でのビルド環境はなさそうで、将来的に対応されることを期待して待つという選択は難しいと判断しました。
https://forum.remirepo.net/viewtopic.php?id=4016

そのため、M1 Macにおける開発環境構築手段として別の方法を考えました。

  • Docker Buildをやめる
    • x86_64で構築済のコンテナをECRにアップロードし、それを取得して使ってもらう
    • ※Docker for Macのエミュレーション機能で
  • Amazon Linux2をやめる
    • 他のLinux系コンテナで構築する

Docker for Macのエミュレーションに頼るのが一番簡単な解決方法なのですが、本番環境をAWSのGravitonプロセッサで動かす選択肢を捨ててしまいたくなく、Amazon Linux2での構築はあきらめ、別のコンテナOSで構築する決断をしました。

Alpine Linuxで構築

Docker HubのPHP Official ImageはAlpine Linuxベースで提供されています。
https://hub.docker.com/_/php

※このイメージをベースに構築したDockerfileを以下のGitHubリポジトリにアップしました。
https://github.com/LancersDevTeam/PHP_versionup/tree/master/PHP7.3toPHP8.1/docker.alpine/dev/app-admin

Alpineは軽量なDocker Imageですが、インストールするパッケージによっては、makeやgccをインストールしてビルドする必要があり、これを単純にビルドすると、コンテナイメージが簡単に肥大化してしまいます。

イメージが小さくなるようにビルド工夫をしても、結局はライブラリのインストール分だけイメージ容量が増えるので、Alpineが最適な選択になるためには、それなりに条件が必要だと感じました。
(個人的には、Goバイナリで運用する場合はイメージが小さく済むことが多く、最適な選択になりやすいと思いました)

awslogsのインストールが困難な問題

さて、このAlpine Linuxベースで構築したコンテナですが、今度は、awslogsのインストールが困難を極めました。

# pip install awslogs

でインストールされるawslogsは、今回必要なCloudWatch Logs(awslogsd)ではありませんでした。(こちらはツールの様子)

CloudWatch Logsをインストールするには、インストールスクリプト
https://s3.amazonaws.com/aws-cloudwatch/downloads/latest/awslogs-agent-setup.py
を利用するのですが、これがAlpineに対応していないことが判明しました。

手数をかけてAlpineに対応するよりも、別のOSを選択したほうが良いと判断し、Alpine Linuxでの構築も断念しました。

Ubuntu Linuxで構築

Debian系のLinuxであれば、PHP8.1、awslogs共にインストールできることがわかり、最終的に、Ubuntu Linuxで構築しました。

※このイメージをベースに構築したDockerfileを以下のGitHubリポジトリにアップしました。
https://github.com/LancersDevTeam/PHP_versionup/tree/master/PHP7.3toPHP8.1/docker/prd/app-admin

インストールスクリプト
https://s3.amazonaws.com/aws-cloudwatch/downloads/latest/awslogs-agent-setup.py
がサポートするPythonのバージョンはPython2.6~3.5ですが、Ubuntu 22.04 LTSコンテナにインストール済のPythonのバージョンは3.10のため、そのままではインストールできませんでした。

※本件はPython2.7を入れて解決させましたが、Python3.10でもビルドできるようにすればさらにコンテナイメージを減らすことができそうで、今後の課題にします。

※awslogsのインストールは @adachin0817 にも協力いただきました。
インストール手順の詳細内容を ADACHIN SERVER WIKI に記載しています。

現在、このUbuntuベースのコンテナでPHP8.1を運用できています。

さいごに

PHP8.1バージョンアップにようやく着手しましたが、インフラ面において、要件を全て満たすコンテナOSの選択という点で早々と苦労することになり、まずは、このネタだけでブログを書くことにしました。

次回からは、PHP8.1バージョンアップに伴う具体的なソース修正についても書いていきたいと思います。

本ブログ内容の詳細は、2022/8/27に開催されるPHPカンファレンス沖縄2022 で発表予定です。

10年続くプロダクトの技術変遷と3つの転機

kanazawa|2021年12月13日
AWS

こんにちは。 ランサーズの金澤です。私は2013年の11月にランサーズにジョインして以降、バックエンド、インフラの構築に携わってきました。

私が入社した頃はちょうどクラウドソーシングサービスの広がっていた時期で、ランサーズのユーザ数も右肩上りで増えていきました。もちろんサービスが増え成長していくに当たってインフラ体制も変化。さまざまな課題に対して変更を加えることで対応してきました。

今回はランサーズのバックグラウンドのプロダクト技術をどのように変遷させていったのか。これまでのことを思い出しながらお話していこうと思います。

10年を超える開発で出会った3つの技術的転機

ランサーズは2008年に創業してから、クラウドソーシングサービスLancers.jpを皮切りにサービスを展開してきました。2011年の東日本大震災以降、リモートワークという働き方が注目され始め、社会的な時勢を背景に2012年には利用者が10万人を突破。その1年後の2013年には20万人を越えるなど一気に成長していきました。

私は入社以降、インフラエンジニアとして会社の成長と共に、サービスを支えるバックエンド、インフラにアップデートを加えていきました。

ランサーズでのバックエンド、インフラの大きな変化は3つあります。1つ目はサーバを既存のサーバからAWSへと移行したこと。2つ目はDockerの導入。そして3つ目はPHPのバージョンをアップデートしたことです。

これら3つの技術転換を行ったことで、ランサーズのインフラ体制は大きく改善されていきました。3つの転機について順に解説していきます。

転機1:日本国内でもいち早く進めたAWS移行

1つ目の転機はAWSへの移行です。これはユーザが急増した2012年5月頃のことで、私がランサーズへの転職を決意したきっかけでもあります。現在ではほとんどの企業が活用しているAWSですが、当時はまだまだ認知も低く、導入している企業もとても少なかったです。そんな中、ランサーズはいち早くAWSの導入を決めました。当時としては思い切った判断だったと思います。

私もAWSを知った時は、これはすごい技術だと思いました。近い将来AWSのようなクラウドインフラが主流になり技術の概念が変わる。そう感じていました。そこでもっと実務レベルでAWSを触りたいと思いランサーズへの入社を決意。以降は念願だったAWSを使ったバックエンド構築に携わらせていただきました。

私が入社した頃は、AWSへと完全に移行を終えていましたが、データベースに関してはAWSのマネージドサービスであるRDSを利用していない状態でした。

その理由としてはまだRDSのストレージパフォーマンスが悪かったことと、細かいレベルでのログを見られなかったという背景がありました。

しかし、その頃にインデックス要因で不整合が起こりサービスが丸2日止まってしまうといったトラブルも起きていたため、RDSへ移行しながら解決してくれと上司にお願いされたのが私の最初の仕事でした。


▲右側(移行後)が、入社直後のシステム構成 (【JAWS DAYS 2014】ランサーズを支えるRDSより)

AWSの技術によって作業が効率化し、サービスも安定するように

データベース移行のためにAWSに触れたのですが、やはりその技術に衝撃を受けたことを今でも覚えています。中でも一番驚いたことはリードレプリカを使ってデータベースの複製を右クリック1つで行えたことです。当時データベースを複製するには、タイミングを合わせてログの位置を確認し更新日程を確認して作らなければいけなかったのですが、その工程が一瞬で完了してしまうというのは信じられませんでした。

作業効率でいえば普段、1時間以上かけて慎重に行う作業が1分でできるほど向上しました。

AWSはまだ進化の途中でもあったので、その後も2年くらいかけてアップデートされたものを置き換えていって、安定して動かせるサービスへと落ち着かせていきました。

将来性のあるクラウドインフラをいち早く導入して整えていった。これがランサーズの最初の転機でした。

転機2:開発環境の「社内サーバ → Docker移行」

2つ目の転機は2015年頃に開発環境をDockerに切り替えたことです。当時はWindowsPCを使って社内のサーバにアクセスして開発していた状態だったのですが、会社に来ないと開発を進められないというのがとても不便でした。また、新しい開発環境になったときに人数分だけ手動でアップデートをしなければいけないという手間や、ディスクが足りなくなるなどの物理的な問題も生じていました。当初は私が秋葉原までサーバを買いに行くくらいギリギリの状態でした(笑)。

サービス拡大とともに、スケーラビリティのあるDockerへ

2012年まではサービスがほぼ単一だったので、社内サーバにアクセスしてみんなで開発するということもできていました。しかし徐々にサービスの規模や社員数も増え、不便を感じ始めるとVagrantを活用するメンバーが出てきました。スペックの高いパソコンを持つメンバーが自前のPCにVirtualBoxを入れてLinuxを立ち上げるというやり方で2013年ごろから2年ほど運用していました。

しかし2015年ごろに限界がきました。大規模な案件に対応した「Lancers for ビジネス」や法人向けのアカウントサービス「Enterprise」といったサービスも増え、開発環境も増えたのでディスクが足りないといった問題も起きるようになりました。サービスは今後も増えるのにこのままではやっていけないという話になりDockerを入れようという決断に至りました。

実は、私は最初Dockerに反対派でした。Dockerの機能や将来性に未知数な部分があったからです。なのでまずは1つのVirtualBoxに入れてみたらどうかと思い試してみたのですが、環境ごとにPHPのバージョンが違ったりとか、Rubyの環境だとかを整えるのがあまりにも大変でした。これは1つにまとめるのは無理だという結論になり、最終的にDockerに賭けるという形で導入をしたのですが、結果的に大成功でした。

アップデートも自分が作ったものをDocker内のリポジトリに上げておくだけで、メンバーがそれを更新するだけで済んだりと、少ない作業でメンバー全員の開発環境を整えられるのはとても楽になりました。

私は新しい技術に対して飛びつく方ではなくて、色々とメリットデメリットを吟味した上で慎重に取り組む方なのですが、この時は思い切って試して成功するという貴重な経験ができたと思っています。

▲2019年時点のランサーズのシステム構成図( 成長サービスのDB負荷問題を解決するより)

転機3:PHPの一大アップデートプロジェクトで処理速度が4倍、費用は4分の1に

3つ目の転機は2017年から2019年にかけてPHPのアップデートを行ったことです。ランサーズでは創業当初からPHPを使っていたのですが、常に最新バージョンへアップデートしていたわけでありませんでした。2017年まで使っていたPHP5.3はもうサポートが終わっており、アップデートをこれ以上先延ばしにできないと感じていました。

私はインフラエンジニアとしてサーバのスピードアップや処理速度をあげることに力を注いでいるのですが、PHPを7にアップデートすると処理速度が2倍以上になることがわかっていたので、いずれやらなければと考えていました。

アップデートはプロダクト開発と比べて事業へのインパクトが見えづらいため、優先順位が下がってしまっていたのですが、2017年の3月ごろからようやく本腰を上げて行うようになりました。

処理速度が大幅に上がり、サーバ費用も削減

PHPのアップデートは、古い機能から新しい機能への置き換えが必要で大変な作業なのですが、最も大変だったのはフレームワークであるCakePHPのアップデートです。この作業では、コードを読んでいって該当するところを洗い出して置き換えていく必要があります。一般的には一旦開発を止めてコードの洗い出しに集中するのですが、ランサーズの場合はコードの規模も大きく開発を止めることができなかったため、新しいバージョンと古いバージョンを共存させながら少しずつ置き換えていきました。

共存させながらのアップデートは、一度始めたら最後までやり切る必要があります。途中で挫折してしまうと新しいバージョンと古いバージョンが中途半端に残ってしまい、以前よりもメンテナンスしにくくなってしまいます。

過去にこの状態に陥ったケースに関わっていたことがあるのですが、ランサーズでは絶対にやりきるぞという決心のもと最後までやり切りました(笑)。

約2年という長い時間をかけて無事に2019年にアップデートが完了したのですが、改善された環境にとても感動しました。処理速度は体感で4倍近くになったことでサーバ費用も4分の1に削減できました。メンバーも仕事効率が上がったと喜んでいて、諦めずに取り組んでよかったと感じています。

さらに、最新のバージョンにアップデートしたことでCakePHPのOSSに貢献したり、コアデベロッパーの方々と繋がれるようになりました。

最新のバージョンにキャッチアップし続けるメリットには、使っているOSSなどで問題が起こった場合に、OSS自体に修正提案を行うことができることです。ここでバージョンが古くてサポート期限が終了していると、プロダクト側で余計な修正を行う必要がでてきて、それがさらにバージョンアップの足枷になったりして悪循環に陥ります。

また、最新の技術についての意見交換できる場を作れることで、自分自身の技術力の向上に繋がりアップデートの意義はかなりあったと感じています。

10年超のプロダクトにおける技術スタックでは”技術の見極め”と”断捨離”が大事

ランサーズは、Lancers.jpというプロダクトの成長に合わせて技術スタックも様々な改良を加えてきました。運用10年を超えるプロダクトに関わるインフラエンジニアとして考える、長期のプロダクトの技術スタックで重要なことは「技術を見極めた上で断捨離すること」です。

ベンチャー企業においては、流行りの技術を導入することは、エンジニア採用の面においても重要だと感じています。

一方で、その技術が確実に市場に浸透するか見極めることが更に重要だと感じています。

流行る技術はたくさんあるのですが、実は市場に生き残れる技術は一握りです。
一度コアな技術として取り入れると、そこから移行することは困難なので、中長期で活用できる技術なのかという視点を持って、実用性を吟味していくことが大事だと思います。

また、新しい技術を取り入れたら古くなった技術を必ず捨てることも大切です。新しい技術を積極的に取り入れる会社は多いですが、古くなった技術をそのまま放置してしまいがちです。ランサーズのPHPアップデートの事例でも見たとおり、古い技術を残したままだと、開発環境がごちゃごちゃしてしまったり意外なところで干渉していたりとデメリットが多いので、新しい技術を取り入れるのと同じくらい、古い技術を置き換えることに目を向けることが大事です。これによって将来的なタスクが減っていき、プロダクトを長い時間をかけて作っていくための地盤ができていきます。。

インフラエンジニア目線の意見になりましたが、このお話が少しでもプロダクト開発の参考になれば幸いです。

CakeFest2021への登壇とその後の影響について

kanazawa|2021年12月09日
CakePHP

ランサーズ Advent Calendar 2021 9日目の記事です。

ランサーズの金澤です。
現在はSREチームとQAチームを兼任しております。

QAチームの取り組みについては様々ありますが、現在は「レガシーの排除」を掲げ、
主にCakePHPのバージョンアップ(CakePHP2→CakePHP4)をメインに取り組んでいます。

CakeFestについて

CakeFestはCakePHPの国際イベントです。
毎年、世界各地で開催され、2019年は日本でも開催されました

https://cakefest.org/

ランサーズは2017年からCakeFestへのスポンサーを開始し、
2020年はゴールドスポンサーになっています。

CakeFest2019 東京開催での登壇

2019年の東京開催では、Proposalが採択され、登壇する機会をいただきました。
CakePHP3への滑らかな移行を考えるというテーマで発表いたしました。

この時の様子は、過去のエンジニアブログでも紹介させていただいております。

CakeFest2019で英語発表するために準備したこと

CakeFest2021 リモート開催での登壇

CakeFestは2020年以降はリモート開催となっております。

2021年もリモート開催となり、再び登壇の機会をいただくことができました。

CakeFestは国際イベントのため、英語での発表になります。
2019年の東京開催のときは、リアルタイム通訳のサポートがあったため、英語発表とは言えど、何とかなりそうだという安心感がありました。

しかしながら、今回は通訳なしのため、東京開催とは違った緊張感がありました。
進行中のリアルタイムでのやり取りに関しても自分で対応する必要があります。

心強かったのは、もう一人の日本人発表者のコネヒトの伊藤さん(@itosho)がいらっしゃったことです。
本番前に2回ほどリハーサルが行われましたが、この時も一緒に準備をさせていただきました。

※伊藤さんも当日の様子をブログにしております。

CakeFest2021 の発表内容

今回の発表内容は、「CakePHP4で自動的にMasterとReplicaを分散させる」という内容でした。

CakePHPは、データベースのMasterとReplicaを分離する仕組みをまだ用意していません。
分離が可能であれば、参照クエリを複数のリードレプリカに分散できるのですが、その仕組みがないため、プラグインを使って実現します。

このプラグインを開発したのが、元コネヒトの金城さん(@o0h_)です。
詳細は以下のブログにまとめられています。
https://tech.connehito.com/entry/cakephp-master-replica

ランサーズは、CakePHP2移行完了後にすぐにCakePHP3バージョンアップに取りかかりました。
2019年の発表の通り、CakePHP2と共存しながらのバージョンアップ体制を整えましたが、
管理画面とバッチについては新規にCakePHP3プロジェクトとしてリポジトリ分離し、
スクラッチ開発しています。

その際、データベースの分離の仕組みについては、このプラグインを採用させていただきました。

※CakePHP4リリース後は、ランサーズ側でCakePHP4対応をさせていただきました。
https://github.com/Connehito/cakephp-master-replica/pull/3

このプラグインを利用すると、CakePHPのデータベース設定において、複数のコネクションを定義することができるようになります。

Master用のRoleとReplica用のRoleをそれぞれ定義し、参照クエリを発行する直前に、Replica用のRoleに切り替え、書き込み用のクエリの直前にmasterに切り替える、といった実装を行うことで負荷分散を実現させます。

しかしながら、単純にこの実装を行うと、ソースのいたるところに切り替え処理がちりばめられてしまい、煩雑なソースコードとなってしまいます。

※現在運用中のCakePHP2では、この切り替え処理を独自実装していますが、同様にソースが煩雑になってしまっています。

アプリ開発者側の立場に立つと、開発環境ではこのような分散を意識する機会がありません。
また、煩雑な実装になることを嫌がる事情もあり、結果、Replica分散が徹底されない状態になってしまいます。

理想的なのは、参照クエリを発行するときに自動的にRead Replicaに切り替えることです。

今回の発表では、Tableクラスを継承し、クエリ発行関数をオーバーライドして、直前に切り替え処理を挿入することで、自動的に切り替えを実現させる方法を紹介しました。

具体的な実装についてはQiitaGithubで紹介していますので、興味がある方は参照して合わせてご覧ください。

発表当日の失敗談

リハーサルでは、発表順にカメラの表示、スライドの共有チェック等を簡単に行いましたが、本番のプレゼンの表示で失敗をしていたことに後で気づきました。

2019年の東京開催でも、今回の開催でも、英語で発表するにあたり、スライドのノートに翻訳内容を事前に記入しておきました。
PowerPointは、発表の際は外部スクリーンに全画面を表示し、手元PCのモニタには次のスライドと翻訳のカンペを表示させることができます。

リモート発表においてもその機能を利用することは可能で、リモート先では全画面表示にして、自分の手元ではカンペを表示することができます。

事前の社内リハーサルにおいては、ZoomでもGoogle meetでもその機能が使えることを確認していました。(ただし確認は、リモート先の見ている人に口頭で確認が必要)

CakeFestでは、リモートのMTGツールにRing Centralというものを利用していたのですが、これは、その機能が有効にならず、視聴者側にカンペが表示された状態で発表してしていました。

発表後に日本人の視聴者から教えてもらって、相当落ち込んだのですが、他の世界各地の視聴者はそれを気にする素振りも全く見せておらず、何事もなく自分の発表は終わってしまいました。
(自分が気にしすぎなだけ?)

発表当日のビデオがこちら(自分は見ていませんが。。)

その後の影響について

今回、私のProposalが採用された背景として、CakePHPにもMasterとReplicaを切り替える仕組みの導入を検討している背景があったようです。

以前、議論されてたIssueがこちら。
https://github.com/cakephp/cakephp/issues/9197

CakeFest終了後に、CakePHPのSlackで発表内容についての質問を受けました。

最初、視聴者からの質問だと思って答えていたのですが、話していくうちに、CakePHPのコアデベロッパーの方だということがわかりました。

CakePHPにMasterとReplicaを分離する機能を標準で欲しいかを知りたかった様子でした。

そこで、

・MasterとReplicaを分離する機能は標準で欲しい
・MasterとReplicaでそれぞれ別のコネクションを持ち、いちいち切り替えなくても良くしたい
・Replica Lagは常に難しい問題だが、自分の環境ではAWS Auroraにしてからその問題は無視できるようになった

という意見、およびリクエストをさせていただきました。

これをきっかけに、CakePHPに読み取りと書き込みの両コネクションをサポートするIssueが始まっています。
https://github.com/cakephp/cakephp/issues/16095

最後に

国際イベントの中で、自分の発表内容がどれだけ意味のあることか、また、行っているアプローチが妥当なものかどうか常に自問自答しながらの発表でしたが、その後の視聴者やコアデベロッパーの方々からリアクションをいただけたことは、登壇した甲斐あったと思えました。

ほんの少しですが、CakePHPへの開発に貢献できたと感じることもでき、良い体験ができたと思います。

SendGrid用のMailモックコンテナを作りました

kanazawa|2021年08月12日
Docker

SREチームの金澤(@yakitori009)です。

社内開発用にSendGrid用のMailモックコンテナを作りました。

開発環境の構成

検証AWS環境の構成

今回、その経緯と内容について書きたいと思います。

Mailモックコンテナについて

開発環境におけるメール送信テストで最も気を付けるべきことはメールの誤送信です。
メールの誤送信は、最も発生件数の多いセキュリティインシデントの1つで、

開発中のメール誤送信も対策が必要で、最低限、以下の処理を行っておく必要があるでしょう。

  • DBデータのマスク処理
  • メールアドレスを存在しないメールアドレスにマスクする
  • メール送信処理をモック化する
  • 実際のSMTPサーバーに飛ばさず、ダミーのSMTPサーバーに吸収させる

ダミーのSMTPサーバーとしては、MailTrap等のサービスが有名ですが、最近では、メールモックのDockerコンテナを使う事例も増えています。

代表的なメールモックコンテナとして、MailCatcherMaildevなどがあります。

SendGrid用のメールモックコンテナについて

MailCatcherMaildevは、SMTP送信用です。

SendGridは(SMTPサーバーとしての利用も可能ですが)主にWebAPIによるメール送信が一般的かと思われます。

しかしながら、このWeb APIをサポートしたメールモックコンテナは公式では用意されていません。
※SendGridでは、メールのテストを行うために、サブユーザー機能等を提供しています。

SendGridのメールモックコンテナを自作した背景

※公式ではありませんが、SendGrid用のメールモックコンテナが提供されています。

このコンテナ(yudppp/simple-sendgrid-mock-server)には大変お世話になっており、全社共通のツールとしてここ数年使わせていただいてました。

しかし、利用ケースの拡大に伴い、以下の要望が出てきました。

  • 添付ファイルの中身を確認したい
  • 50KB以上のファイルを添付したい(現状、エラーとなる)
  • リクエスト、レスポンスも実際のSendGirdのものと一致させたい
  • 各種エラーメッセージの対応
  • 厳密なパス対応(スラッシュが連続した場合、エラーとする等)

これらを解決させるために、自前で本格的なメールモックコンテナを作ることにしました。

自作SendGridメールモックコンテナの構成

私自身、バックエンドのAPI周りは開発イメージをしやすいのですが、フロントエンドにそこまで強いわけではなく、メールの表示画面については作ることを躊躇していました。(作ってもイケてるUIにならなそう。。)

今回、自前で作る部分を最低限に済ませたく、以下の構成で作ることにしました。

  • 自前で作るのは、SendGridのモックAPIのみ
  • このAPIの処理内で、SMTPでMailCatcherやMailDev等のSMTPメールモックに飛ばす
  • Dockerコンテナには自作のSendGrid APIモックと既存のSMTPメールモックをバンドルする

軽量コンテナにするための方針

Dockerコンテナは、なるべく軽量にしたいところです。
ゆえに、極力不要なものは組み込まない方針にしました。

※結果的に以下のミドルウェアは組み込まずに構築しました。

・WWWサーバー(nginx等)
 ・SMTPメールモックとSendGrid APIはそれぞれ別ポートで起動する
・Appサーバー(php-fpm、unicorn等)
 ・言語が提供するアプリケーションサーバーを利用
・SMTPサーバー(postfix等)
 ・SMTPメールモックのSMTP機能を利用する

SMTPメールモックの選定

SMTPメールモックは(今まで利用したことのある)以下の2つに絞りました。

MailCatcher
 ・代表的なSMTPメールモック
 ・SendGrid MailCatherみたいな名前にすると何をするものなのか一発で理解できる
 ・CC、BCC等がTOに表示されてしまう
 ・必要なミドルウェアが多い(ruby、sqlite、openssl等)
Maildev
 ・Node.jsのみで起動
 ・ISO-2022-JPの表示ができない(ソースを見ないと確認できない)

結果、MailDevを採用しました。

・SendGrid APIを利用する送信にISO-2022-JPを利用する機会がなかったこと
・インストールの手軽さ、軽量さ
・UIがイケてる

などが主な理由です。

※MailCatcherについては、CC、BCC表示の問題や、コンテナの軽量化の方針に向かなかったことが大きなネックになりました。

APIサーバーの言語選定

以下の言語を候補に入れていました。

・PHP
 ・一番慣れている言語
・Ruby
 ・MailCatcherが利用している(共用できるかも?)
・Node.js
 ・MailDevが利用している(共用できるかも?)
・Golang
 ・バイナリのみで動作可能(コンテナにGolangのインストール不要)

結果、Golangを採用しました。

Golangであれば、ビルドしたバイナリを配置するだけでよく、言語すらインストール不要になります。

※極力軽量なコンテナにする方針だったためこの選択は後悔していませんが、バイナリならではのデメリットが後に生じました。(後述のM1 Mac対応を参考)

採用したFW、ライブラリ

Golangにおいて、以下のFW、ライブラリを採用しました。

・echo
・apitest
・validator.v9

※このAPIサーバーの実装はGitHubで公開しています。

SendGrid API + MailDevコンテナの構築

※構築したコンテナはDockerHubで公開しています。

Dockerコンテナの構築にあたっては、2つのプログラムを同時に起動する必要がありました。

・自作のSendGrid APIサーバー
・MailDevサーバー

この2つを同時起動させるため、supervisordで制御しています。

DockerHubのイメージのビルドは、GitHubリポジトリと連動させており、masterにpushしたら自動的にDockerHubのビルドイメージが更新されるようにしています。

M1 Mac(arm64)対応について

最近はArm系のCPUを搭載したM1 Macが出回り始めたため、Dockerコンテナを構築する際は、linux/amd64に加え、linux/arm両方への対応を意識する必要がでてきました。

DockerHubに登録されている代表的なコンテナは、Intel系、Arm系を含め、複数のアーキテクチャに対応しているものが多いです。

ただし、M1 MacでもRosetta2やDocker for Macの互換性維持機能があり、linux/amd64でも動作できるものが多いです。

今回構築したコンテナについては、結果的にlinux/amd64でも動きました。
また、本家maildevコンテナも現状linux/amd64のみの提供となっており、一旦linux/amd64のみでサポートしております。

ただ、linux/arm64にも対応することで、以下のメリットがあります。

・互換性維持の処理がなくなり、パフォーマンスが良くなる
・AWS EC2のGraviton2インスタンス上でも動作できる

将来的にマルチアーキテクチャをサポートしたコンテナにしたいと考えています。
今回、(途中で挫折しましたが)linux/arm64にも対応するために行ったことを残しておきたいと思います。

マルチアーキテクチャに対応したDockerイメージの構築

x86_64とarm64両方に対応するコンテナイメージを構築するには、buildxという機能を使います。

以下のようにdockerコマンドを実行することで、linux/amd64、linux/arm64両方に対応したコンテナをビルド可能です。

$ docker buildx build --platform linux/amd64,linux/arm64 -t ykanazawa/sendgrid-maildev:latest --push .

Golangのバイナリのアーキテクチャ別にビルド

Golangでビルドを行う際に、x86_64、arm64のバイナリをそれぞれビルドしておきます。

x86_64

$ env GOOS=linux GOARCH=amd64 go build -o sendgrid-dev_x86_64 main.go

arm64

$ env GOOS=linux GOARCH=arm64 go build -o sendgrid-dev_aarch64 main.go

※PHPやRubyであれば、Dockerfile構築時に、それぞれのアーキテクチャのPHP、Rubyをインストールできるのですが、Golangの場合は原則、ビルドしたバイナリを配置する形になるので、マルチアーキテクチャのコンテナを構築する際はここら辺が面倒になってきます。(言語自体入れなくて良いというメリットと引き換えで生じるデメリット)

Dockerコンテナ構築時にアーキテクチャ別のGolangバイナリを配置

※ここがうまくいかなくて挫折しました。

以前、M1 Macに対応するためにで行っていた方法では、

RUN apt-get update -y && \
apt-get upgrade -y && \
apt-get install -y --no-install-recommends \
...
linux-headers-$(if [ $(uname -m) = "aarch64" ]; then echo arm64; else echo amd64; fi) \
...

のように、unameコマンドの結果を出力していました。

今回、Linuxのバイナリをコピーするために、

COPY sendgrid-dev_$(uname -a) /usr/local/bin/sendgrid-dev

とやってみたのですが失敗しました。
COPYコマンドではコマンドの実行結果を取得

環境変数を使い

ARCH=$(uname -a)
COPY sendgrid-dev_${ARCH} /usr/local/bin/sendgrid-dev

としてみましたが、これも失敗($ARCHに何も入っていなかった)

この方法で構築できないとなると、手順そのものを変える必要がありそうです。

・x86_64、arm64両方のバイナリをDockerコンテナの/tmpにコピーして、RUNコマンドで分岐する
・Docker build時にGolangバイナリをアーキテクチャに合わせてビルドする

などを検討する必要がありそう。
マルチアーキテクチャ対応については、今後の課題としたいと思います。

2021/08/20追記:
SendGrid Web APIのリポジトリのReleaseにGolangのバイナリをアーキテクチャ別にアップロードし、
SendGrid MaildevコンテナのリポジトリのDockerfileでアーキテクチャ判定してそれぞれcurlでダウンロードさせることで対応しました。

SendGrid Web APIのリポジトリのReleaseにビルド済バイナリをアーキテクチャ別にアップロード
https://github.com/yKanazawa/sendgrid-dev/releases/tag/v0.9.0

SendGrid MaildevコンテナのDockerfileを以下のように修正
https://github.com/yKanazawa/sendgrid-maildev/pull/1/files

RUN curl -L -o /usr/local/bin/sendgrid-dev 
https://github.com/yKanazawa/sendgrid-dev/releases/download/v0.9.0/sendgrid-dev_$(if [ $(uname -m) = "aarch64" ]; then echo aarch64; else echo x86_64; fi)

※$(if [ $(uname -m) = “aarch64” ];…の部分は、単純に$(uname -m)としたかったのですが、この記述だとDockerfileが通らず。

さいごに

今回、社内からの要望をきっかけに本格的なSendGridメールモックコンテナを構築するに至りました。
それなりに手間がかかりましたが、公式でまだこのようなモックコンテナが出てないことと、非公式でも決定版的なものが普及してなさそうなので、今後丁寧にメンテナンスしながら普及させていけたらと思います。

お知らせ

2021/8/25にスペースマーケットさんと合同勉強会を行います。

今回構築したSendGridメールモックコンテナについて話す予定です。
興味がある方は是非ご参加ください。

Docker開発環境のM1 Mac対応

kanazawa|2021年04月06日
Aurora

SREチームの金澤です。

M1 Macが市場に出回り、社内でも業務用PCとして採用し始めました。

従来のMacと比べてもコストパフォーマンスが良く、大方問題なく動作しているため、今後も積極的に採用していきたいところですが、最もMacを利用している開発部では、Dockerが安定して動作することが採用の必須条件になります。

M1 MacはARMアーキテクチャのCPUを採用している関係で、Docker開発環境を問題なく動かすために、いくつかの対応が必要でしたので、今回その内容をブログにまとめます。

ランサーズ開発環境のM1 Mac対応

Docker for Macは、ARMアーキテクチャに対応中ですが、対応版はまだ正式リリースされていません。
※現時点では、3/26にリリースされたRC2版を利用しています。
https://docs.docker.com/docker-for-mac/apple-m1/

開発環境のM1 Mac対応

ランサーズの開発環境は、本番AWSでのサーバー構成をエミュレートした構成になっています。

リバースプロキシコンテナ(ELBコンテナ)

AWSのELBをエミュレートしたコンテナ。社内ではELBコンテナと呼んでいます。

今まで、リバースプロキシの処理にH2Oを利用していましたが、M1 Mac環境だとこの処理でタイムアウトしてしまいました。Appコンテナ等で採用しているNginxは正しく動作したので、H2OからNginxに変更するしたところ、正しく動作することを確認。

H2OがM1 Macで動かない原因の詳細までは調べ切れませんでしたが、取り急ぎワークアラウンドとしてNginxに変更することで対応しました。

Appコンテナ

ランサーズのAppコンテナは、x86_64環境でビルドしたコンテナをAWS ECRにpushし、それを開発者がpullして利用する形をとっています。

pullしたコンテナは、M1 Mac上でも問題なく動作しました。

MySQLコンテナ

ランサーズのMySQL5.7コンテナは、x86_64環境でデータをインポートしたものを、同様にAWS ECRにpushしています。

このコンテナも同様に、問題なく動作しました。

他開発環境のM1 Mac対応

ランサーズ以外のサービスについては、開発環境、本番環境共にDockerで運用しています。
ランサーズ開発環境と違い、ECRからpullせず、すべてのコンテナをPC上でビルドしていました。

Appコンテナ

一部のサービスでビルドに失敗しました。

% docker-compose up -d 
Docker Compose is now in the Docker CLI, try `docker compose up` 

Building app [+] Building 68.3s (10/39) => [internal] load build definition from Dockerfile 

... ------ > 
[ 6/35] RUN apt-get update -y && apt-get upgrade -y && apt-get install -y --no-install-recommends bash build-essential default-mysql-client git libcurl4-openssl-dev libghc-yaml-dev libqt5webkit5-dev libxml2-dev libxslt-dev libyaml-dev linux-headers-amd64 locales nginx nodejs openssl python3-pip ruby-dev ruby-json ssh sudo supervisor tzdata vim yarn zlib1g-dev && apt-get clean -y && rm -rf /var/cache/apt/archives/* /var/lib/apt/lists/*: #9 0.223 Hit:1 http://deb.debian.org/debian buster InRelease #9 0.223 Hit:2 http://security.debian.org/debian-security buster/updates InRelease 
#9 0.265 Hit:3 https://deb.nodesource.com/node_12.x buster InRelease ... 
#9 17.04 This may mean that the package is missing, has been obsoleted, or 
#9 17.04 is only available from another source #9 17.04 
#9 17.07 E: Package 'linux-headers-amd64' has no installation candidate 
%

原因は、Dockerfileで以下のパッケージををインストールしていた箇所です。

linux-header-amd64

linux-headerについては、アーキテクチャ別にパッケージが用意されています。
M1 Mac上では、

linux-header-arm64

をインストールする必要があります。

M1 Macの場合はarm64版をインストールするために、以下のようにしました。

linux-headers-$(if [ $(uname -m) = "aarch64" ]; then echo arm64; else echo amd64; fi)

※ちなみに、以下の指定による解決方法も考えましたが、これはどちらも正しくインストールできませんでした。

linux-header-$(uname -m) 
linux-header-generic

MySQLコンテナ

ランサーズで利用しているMySQLのバージョンは5.7です。
AWSで利用しているRDS AuroraがMySQL5.7ベースなので、開発環境もそれに合わせたものを利用しています。

ところが、オフィシャルで提供しているMySQL5.7コンテナはArm64をサポートしていません。
(MySQL8.0はサポートしている)

そこで、以下の解決方法を考えました。

  1. x86_64環境でビルドしたコンテナをECRにpushし、それをpullして利用する
    ・ランサーズのMySQLコンテナが正しく動作することを確認済
  2. MySQL5.7に相当するMariaDBをインストールする

2.については、Maria DBでJSON型をサポートしていない点が、互換性の面において問題となる可能性が高いと判断し、見送りました。
1.については、ランサーズ開発環境で問題なく動作することを確認できていました。
x86_64環境でDockerfileをビルドしたMySQLコンテナをECRにpushしておき、M1 Mac環境でそれをpullして使うことは可能です。

今回、1. の方法を選択し、正しく動作することを確認しました。

おまけ:AWS Graviton環境での動作検証

ランサーズでは、PC上で動作するDocker開発環境に加え、開発中の機能を検証するためのBackStage環境をAWS環境に構築しています。
EC2インスタンスにDockerをインストールし、PC開発環境と同じDockerコンテナで動作させる仕組みです。

今まで、この環境をt3a.mediumインスタンスで運用していましたが、Arm64に対応したため、AWS Graviton環境のt4g.mediumでも動くか検証してみました。

結果、正常に動いたのはRedisコンテナと、ELBコンテナのみでした。

Redisコンテナは、Arm64にも対応したマルチアーキテクチャのコンテナで、ELBコンテナも、マルチアーキテクチャのalphineコンテナをベースにビルドする形にしていたため動きましたが、それ以外のコンテナは起動できませんでした。

Docker for Macでは、M1 Macでもx86アーキテクチャのコンテナが動くように設計されていますが、純粋なArm64のLinux環境ではx86アーキテクチャのコンテナは、(予想通りでありましたが)動かないことを確認できました。

(これらを動くようにするには、Docker Buildのフェーズで、もう少し細かい配慮が必要そうです)

さいごに

今回、弊社開発環境で対応が必要だったことは、以下の3点でした。

・H2Oではなく、Nginxを使う
・linux-headerのインストールをArm64にも対応させる
・MySQL5.7はx86_64環境でビルドしたものをECRにpushしておき、それを使う

多少苦労しましたが、M1 Macで動作させることができたことにより、今後、社内でもM1 Macの導入が進んでいきそうです。

2000年代中盤以降、PC市場、およびサーバー市場がx86_64にほぼ統一されていたため、CPUアーキテクチャの違いを考慮するという状況があまり発生しませんでした。

これはある意味幸せな状態だったのですが、そこに競争力のある新しいアーキテクチャのCPUが、優れたコストパフォーマンスでPC、およびサーバー市場に食い込み始めてきたというのが最近の状況かと思います。

インフラ屋にとっては、以前よりも面倒な状態にまたなりますが、競争により、より優れた製品が市場に投入されていくこと自体は良いことだと思います。

今後は、Dockerコンテナやgolangのバイナリで、複数のCPUアーキテクチャを考慮する必要がある程度出てくると思います。

ソースからビルドする機会も多少増えるかも知れませんね。

ランサーズのリモートワーク化の歴史

kanazawa|2020年12月10日
SRE

SREチームの金澤です。
Lancers(ランサーズ) Advent Calendar 2020 10日目の記事になります。

今年はコロナ渦で急遽リモートワークをすることになりましたが、
それほど混乱せずにリモートワーク環境に移行することができまいた。

ランサーズ自体、「時間と場所にとらわれない働き方の実現」というのが、創業当初からテーマになっていましたので、社内環境も常にリモートワークを見据えて準備していたことがうまく行った大きな要因になっています。

「時間と場所にとらわれない新しい働き方を創る」という入社当時のビジョンを、社内レベルでも実現させたいと考え、社内環境も常にリモートワークを見据えて準備していました。

このブログでは、ランサーズのリモートワーク化の歴史について話していきたいと思います。

金澤入社当時の環境(2013/11頃)

ランサーズは2008年創業の会社です。
最近のスタートアップは、ほぼすべての業務フローをクラウドサービスで整えているため、リモートワークしやすい環境にあると思います。
しかしながら、ランサーズに関しては私が入社した2013/11の時点では、リモートワークを行うにあたり壁となる環境や業務フローが残っていました。

・PC環境
・ほぼ全員デスクトップPC、
・開発環境
・社内にある開発サーバーにログインして開発
・社内サーバー
・リポジトリ用SVNサーバー
・外からVPNでリモートでログインできるのは一部の人だけ
・情報共有用Redmineサーバー
・ファイルサーバー

私自身、インフラエンジニアとして入社しており、社内サーバー周りも見てきましたが、この頃はまだリモートワークの体制が整っていませんでした。

リモートワーク環境構築の方針

リモートワーク環境を構築するにあたり、以下の方針で進めてきました。

・新規に購入するのはノートPCのみにする
・開発もノートPC内で完結できるようにする
・新しい社内用サービスはクラウド版を採用する
・社内サーバーはいずれは完全に撤廃する
・社内へのVPNを作らない
・社内LANに入れても何も得るものがない状態にする
・社内のみに限定していたアクセスはプロキシサーバーを通す

ノートPC化

新規に購入するものはノートPCにする。
今後デスクトップPCは購入しない。

開発環境のリモート化

クラウドサービスへの移行

・SVN
・Github
・Redmine 情報共有
・Confluence
・Googleドキュメント
・弥生会計
・勘定奉行(Azure上で構築)

社内サーバーのクラウド化

社内サーバーは2019年中に全てAWSに移行しておきました。
※今思えば、この時に完遂しておいて本当に良かったと思います。

※AWS移行の詳細は以下のブログを参照

8年間お疲れ様でした!社内サーバーをAWSに移行したお話

上記で紹介したサーバー以外にも、以下のような社内サーバーもありました。

・社内DNSサーバー
・Route53に移行
・社内メールサーバー(開発環境用)
・各PCのMailのモックコンテナに移行
・社内ファイルサーバー(Tera Station)
・Googleドライブに移行

ちなみに、サーバーを社内のビルで運用すると、年1回のビルの保守点検で停電が発生するので、その対応をする必要があります。
ランサーズが入居しているビルは毎年5/4に停電があるのですが、GW中の停電はなかなか辛いものがありました。
この対応がなくなっただけでもかなり楽になりました。

分析SQLの発行環境

社内からAWS上のDBに対して、分析用途でSQLを発行できる仕組みを整えていましたが、

リモートワーク化により、社内にとどまらず発行する必要が出てきました。

古典的ですが、phpMyAdminを導入しました。
phpMyAdminは専用のコンテナも提供されており、ECS Fargateで運用しています。
https://hub.docker.com/r/phpmyadmin/phpmyadmin/

リモート用プロキシサーバーの構築

クラウド化したサービスは、原則、二要素認証の設定を必須にしますが、サービスによっては二要素認証をサポートしていないものもあります。
(例えば、先ほど述べたphpMyAdminの画面など)

そのようなサービスは、今までは社内からのみアクセスを許可していましたが、リモート主体となってからは、認証つきプロキシサーバーを経由してアクセスさせるようにしました。
プロキシサーバーからのアクセスにのみ許可することで第三者からのアタックを防いでいます。

リモート槍

リリースの重複を防止するために、リリース時に槍を持つ文化がありましたが、

リモート主体となり、それができなくなったため、Slackで仮想槍を持つ仕組みを作りました。

最後に

実際にリモートワーク主体で業務を行うようになりましたが、
特に弊害もなく、むしろ開発部においては、リモートワークの方が効率が良い場面も多いと感じてきました。

特に、

・会議室が要らない(集まるロスもない)
・画面共有の手軽さ

などの場面でリモートワークの優位性を感じています。

まだ、通常業務において希に出社が必要な場面にも遭遇しますが、現在、全社的に物理業務の撤廃に取り組んでおり、出社が必要な業務が完全に撤廃される日もそう遠くないと感じています。

CakePHP2→CakePHP4へのジャンプアップ

kanazawa|2020年03月31日
CakePHP

SREチーム、QAチームの金澤です。

昨年より、CakePHP2→CakePHP3の段階的バージョンアップに取り組んできましたが、
バージョンアップ対象をCakePHP3からCakePHP4に変更しました。

CakePHP2→CakePHP4にターゲットを変更する

CakePHP3へのバージョンアップを開始したのは、2019/8です。
※バージョンアップ開始時のブログはこちら

この時はまだCakePHP4は正式にリリースされていませんでしたが、
CakePHP2→CakePHP4へのジャンプアップできるか調査したところ、
PHPUnitのバージョンが合わず、見送っていました。

CakePHP2で利用できるPHPUnitの最新バージョンはPHPUnit5.7です。
CakePHP3では、PHPUnit5.7が共存できたのですが、
CakePHP4で利用できるPHPUnitの最低バージョンはPHPUnit8.5になります。

PHPUnitの共存

しかしながら、CakePHP同様、PHPUnitも共存できるのではないかと考えました。

それぞれ、以下のディレクトリに配置し、共存します。

CakePHP2:PHPUnit5.7 (cake28/Vendor/phpunit)
CakePHP3:PHPUnit6,0 (vendor/phpunit)

具体的には、以下の手順になります。

・PHPUnit5.7をvendor/phpunit からcake28/Vendor/phpunitに移動
・composer管理からgit管理にする
・composer管理のvendor/phpunit をPHPUnit6.0にバージョンアップ
・CakePHP3が対応する最上位バージョン
・cake28/Vendor/phpunit/phpunit でcomposer install
・※cake28/Vendor/phpunit/phpunit/.gitignoreからvendorを外しておく

$ php composer.phar install
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 25 installs, 0 updates, 0 removals
  - Installing sebastian/diff (1.4.3): Loading from cache
  - Installing sebastian/resource-operations (1.0.0): Loading from cache
  - Installing sebastian/version (2.0.1): Loading from cache
  - Installing sebastian/environment (2.0.0): Loading from cache
  - Installing sebastian/code-unit-reverse-lookup (1.0.1): Loading from cache
  - Installing phpunit/php-text-template (1.2.1): Loading from cache
  - Installing phpunit/php-token-stream (1.4.12): Downloading (100%)
  - Installing phpunit/php-file-iterator (1.4.5): Loading from cache
  - Installing phpunit/php-code-coverage (4.0.8): Loading from cache
  - Installing phpunit/php-timer (1.0.9): Loading from cache
  - Installing sebastian/recursion-context (2.0.0): Loading from cache
  - Installing sebastian/exporter (2.0.0): Loading from cache
  - Installing doctrine/instantiator (1.0.5): Downloading (100%)
  - Installing phpunit/phpunit-mock-objects (3.4.4): Loading from cache
  - Installing sebastian/comparator (1.2.4): Loading from cache
  - Installing symfony/polyfill-ctype (v1.13.1): Loading from cache
  - Installing webmozart/assert (1.6.0): Loading from cache
  - Installing phpdocumentor/reflection-common (1.0.1): Downloading (100%)
  - Installing phpdocumentor/type-resolver (0.4.0): Downloading (100%)
  - Installing phpdocumentor/reflection-docblock (3.3.2): Downloading (100%)
  - Installing phpspec/prophecy (v1.10.2): Loading from cache
  - Installing symfony/yaml (v3.4.37): Downloading (100%)
  - Installing sebastian/global-state (1.1.1): Loading from cache
  - Installing sebastian/object-enumerator (2.0.1): Loading from cache
  - Installing myclabs/deep-copy (1.7.0): Downloading (100%)
symfony/yaml suggests installing symfony/console (For validating YAML files using the lint command)
sebastian/global-state suggests installing ext-uopz (*)
Package phpunit/phpunit-mock-objects is abandoned, you should avoid using it. No replacement was suggested.
Writing lock file
Generating optimized autoload files

cake28/Vendor/phpunit/phpunit に移動したPHPUnit5.7を参照するように設定します。

cake28/Config/bootstrap.php

define('__PHPUNIT_PHAR__', '');
require ROOT . '/vendor/autoload.php';
require_once APP . 'Vendor/phpunit/phpunit/vendor/autoload.php';

PHPUnitの実行については、それぞれ以下のようにコマンドを実行します。
CakePHP4のphpunit.xmlは新規にtestsディレクトリに配置してphpunit実行時に指定するようにしています。

CakePHP2のUT実行

./cake28/Console/cake l_test app Lib/Function/CryptTest.php

CakePHP4のUT実行

./bin/phpunit --bootstrap config/bootstrap.php --configuration=tests/phpunit.xml tests/TestCase/Lib/Functions/CryptTest.php

CakePHP4へのバージョンアップ

composerの各ライブラリを最新にし、CakePHP4に対応させます。

CakePHP4へのバージョンアップ手順についてはの詳細は、年末のブログで紹介しています。

CakePHP4にバージョンアップできれば、PHPUnitも8.5以上にバージョンアップにされるはずです。

今後のCakePHPバージョンアップ計画

CakePHP2→CakePHP3に段階的にバージョンアップ

現在のランサーズのリポジトリには

・ランサーズ本体
・管理画面
・バッチ

が全て含まれていますが、そのうち、

・管理画面
・バッチ

CakePHP4で新規構築して移行し、lancers本体のソースを徐々に削っています。

この新規構築で得たCakePHP4のノウハウをlancers本体のリポジトリにも反映していく予定です。

CakePHP3→CakePHP4にmigrateしてみた

kanazawa|2019年12月25日
CakePHP

CakePHP Advent Calender 2019 最終日の記事です。

SREチームの金澤です。

2019/12/16にCakePHP4がリリースされました。

ランサーズでは、CakePHP2.10の現システムをCakePHP3に移行中ですが、
同時に、管理画面のソースを別リポジトリに分割し、
CakePHP4で新規構築するプロジェクトも進めています。

CakePHP3で開発をしていたのですが、
CakePHP4が正式リリースされたので、CakePHP4にmigrateしました。
今回、その手順を記録しておきたいと思います。

composer.jsonのCakePHP4対応

CakePHP4.0にアップグレードするためにcomposer.jsonを修正します。

案の定ですが、単純に

"cakephp/cakephp": "^4.0",

として、composer updateしても依存関係でエラーとなってしまいました。

$ php composer.phar update
Loading composer repositories with package information
Updating dependencies (including require-dev)
Your requirements could not be resolved to an installable set of packages.

  Problem 1
    - maiconpinto/cakephp-adminlte-theme 1.1.0 requires cakephp/cakephp ~3.0 -> satisfiable by cakephp/cakephp[3.0.0, ... , 3.8.7, 3.x-dev] but these conflict with your requirements or minimum-stability.
    - maiconpinto/cakephp-adminlte-theme 1.1.0 requires cakephp/cakephp ~3.0 -> satisfiable by cakephp/cakephp[3.0.0, ... , 3.8.7, 3.x-dev] but these conflict with your requirements or minimum-stability.
    - Installation request for maiconpinto/cakephp-adminlte-theme ^1.1 -> satisfiable by maiconpinto/cakephp-adminlte-theme[1.1.0].

$

そのため、まずcakephp/appのcomposer.jsonをベースにCakePHP4.0をインストールし、
これをベースに必要なツールを1つづつ追加していくことにしました。

結果、CakePHP用のAdminLTEがCakePHP4に対応していないことが判明しました。
(冒頭のエラーの通りですが)

AdminLTEのCakePHP4対応

CakePHP用のAdminLTE
https://packagist.org/packages/maiconpinto/cakephp-adminlte-theme
は、CakePHP3専用で、現状CakePHP4には対応していません。

そのため、Githubリポジトリ
https://github.com/maiconpinto/cakephp-adminlte-theme
をforkしてCakePHP4に対応させることにしました。

githubリポジトリのプロジェクトをcomposerで読み込む

forkしたAdminLTEをCakePHP4.0に対応させ、自社のGithubリポジトリ
https://github.com/LancersDevTeam/cakephp-adminlte-theme
で管理します。

forkしたリポジトリのcomposer.jsonのnameを変更します。

  "name": "lancers/cakephp-adminlte-theme",

管理画面プロジェクト側のcomposer.jsonで、そのnameとforkしたgithubリポジトリを呼び出すように設定します。

{
    "repositories": [
        {
            "type": "vcs",
            "url": "git@github.com:LancersDevTeam/cakephp-adminlte-theme.git"
        }
    ],
    "require": {
...
        "lancers/cakephp-adminlte-theme": "dev-master"
    },

関連ツールがCakePHP4に対応するまでは、
こうやって自前でCakePHP4に対応し、githubで管理しておく必要がありそうです。

CakePHP4.0対応のPRを出す

AdminLTEのCakePHP4化についてはPRを出しておきました。
https://github.com/maiconpinto/cakephp-adminlte-theme/pull/93

※CakePHP4版はPHP7.2以上が必須になり、PHPUnitも8.3.3以上が必須になるため、
(PHP5.4やPHP5.6をサポートする)CakePHP3版との共存は難しそうです。
ので、マージされるのであれば、メジャーバージョンアップでの対応になるかも知れません。

Upgrade ToolによるMigrate

4.0 Upgrade Guideに従って、Upgrade Toolによるアップグレードを行います。

※AdminLTEのCakePHP4バージョンアップの際も利用しました。

CakePHP Upgrade Tool

4.xブランチに切り替えて、以下のコマンドを実行すると、

$ ./bin/cake upgrade file_rename templates /path/to/app/
$ ./bin/cake upgrade file_rename locales /path/to/app/

以下の変換が行われます。

・src/Template → templates
・ctp → php
・src/Locale → resource/locale

※これに伴い、app.phpのpathsの設定も変更する必要があります。

config/app.php

        'paths' => [
            'plugins' => [ROOT . DS . 'plugins' . DS],
            'templates' => [ROOT . DS . 'templates' . DS],
            'locales' => [RESOURCES . 'locales' . DS],
        ],

Rector

composer.jsonにrectorを追記してインストールします。

    "require-dev": {
...
        "rector/rector": "*"
    },

4.0 Upgrade Guide に記載されていた、cakeコマンドで起動する方法がうまく行かなかったので

$ bin/cake upgrade rector --rules cakephp40 src
Unknown command `cake upgrade`. Run `cake --help` to get the list of commands.

Other valid choices:

- help
- version
- i18n
- routes
- server
- console
- bake all
- bake
- migrations
- compile

rectorコマンドを直接起動しました。

$ ./vendor/rector/rector/bin/rector process src --set cakephp40
Rector v0.6.0

  0/66 [░░░░░░░░░░░░░░░░░░░░░░░░░░░░]   0%
PHP Fatal error:  Declaration of App\Model\Table\FaqsTable::initialize(array $config) must be compatible with Cake\ORM\Table::initialize(array $config): void in /path/to/app/src/Model/Table/FaqsTable.php on line 98
$

親クラスであるCakePHP4 CoreのTableクラスと引数、戻り値が一致しないと怒られたので
これを修正していきます。(これを自動的に修正して欲しいのですが。。)

結果、以下の修正を行う必要がありました。

public function initialize(array $config)

public function initialize(array $config) : void

public function buildRules(RulesChecker $rules)

public function buildRules(RulesChecker $rules) : \Cake\ORM\RulesChecker

public function validationDefault(Validator $validator)

public function validationDefault(Validator $validator) : \Cake\Validation\Validator

public function beforeFilter(Event $event)

public function beforeFilter(EventInterface $event)

cakephp/app のCakePHP4対応

プロジェクトの以下のディレクトリをCakePHP4に対応させる必要があります。
(PHP7.2に対応に伴う引数の変更や戻り値の型変更など)

・config
・src
・templates
・webroot

※結局、https://github.com/cakephp/app のCakePHP4対応版を上書きし、
個別の実装差分を記述し直しました。

始まったばかりのプロジェクトのため、それほど手間ではありませんでしたが
既にCakePHP3の実装が進んでいるプロジェクトは別のアプローチが必要かも知れません。

stableバージョンにしたいが。。

現状のcomposer.jsonは以下の記述があるため、CakePHP 4.x-devがインストールされます。

    "minimum-stability": "dev"

stable版である CakePHP4.0.0 をインストールするにはこの記述を削除する必要がありますが、
cakephp/migrationsも削除する必要がありました。
このツールはrectorが利用しているため、完全に移行が終了してから削除したほうが良さそうです。

"cakephp/migrations": "3.0.0-beta1",

また、bakeも2.0から1.2.8までバージョンを落とす必要がありました。

        "cakephp/bake": "^1.2",

bakeのバージョンを1に落とすと、さすがにCakePHP4では使えなさそうなので、まだ

    "minimum-stability": "dev"

の設定は外せなさそうです。

最後に

今回、開発中のプロジェクトをCakePHP3からCakePHP4にmigrateしてみましたが、
周辺ツールがまだCakePHP4に追いついておらず、
今のタイミングで完全移行するのは時期早尚かも知れません。

ただ、ランサーズ本体のCakePHP2→4移行については、
段階的に移行しているため、できることならCakePHP2→4へジャンプアップしたいところです。
(それを実現するためには、PHPUnitの共存等も実現する必要がありますが)

CakeFest2019で英語発表するために準備したこと

kanazawa|2019年12月02日
CakePHP

ランサーズ Advent Calendar 2019 2日目の記事です。

SREチームの金澤です。

2019/11/7 – 2019/11/10 にCakeFestが開催され、Conferenceに登壇しました。
(ランサーズは前回の2017年に続き、2019年もスポンサーでした)

CakeFestは国際的なカンファレンスのため、英語での発表になりました。
私自身、英語で発表するのは初めての経験でしたが、どのように準備したのか知りたいという要望が多かったので、そのことを中心に書きたいと思います。

実録ドキュメント的な内容になってしまいましたがご容赦を。

CakeFestについて

CakeFestはCakePHPの世界的なイベントで、2019年は日本で開催されることになりました。
※ランサーズは前回の2017年に続き、2019年もスポンサーになっています。

CakeFestはWorkShop2日、Conference2日で構成されます。

WorkShopではCakePHPの最新機能を利用したアプリ構築のレッスンを行います。

Conferenceでは世界各地から応募されたプロポーザルのプレゼンテーションの場となっています。

プロポーザル提出までの経緯

CakeFestが東京で開催されることが決定し、プロポーザルを募集していたのが2018/12頃だったと思います。
この頃はランサーズのCakePHP1.3→2.8移行の大詰めを迎えていた時期で、プロポーザルは他テーマも含めていくつか提出していました。

「CakePHP3への滑らかな移行を考える」というテーマについては、CakeFestの開催まで1年近くありましたので、その頃にはCakePHP3に着手できているはずという思いで見切り発車で提出したものでした。

登壇の決定

そして、採用されたのが「CakePHP3への滑らかな移行を考える」でした。
登壇が決定したのが2019/5/13で、PHP7の移行作業をしていた時期です。

CakePHP3へのバージョンアップについては世界的に関心のあるテーマであることを改めて感じましたが、
いざ採択されると

・ネタの準備
・英語の準備

と2つ同時に進めないといけないプレッシャーがのしかかってきました。

※実のところ、PHP7までアップグレードできたら、CakePHP3移行はすぐに着手せず、別のフレームワークも含めて皆で検討しても良いかなと思っていたのですが。。。

開発チームのメンバーもCakePHP3自体への関心は高かったし、私自身はCakeFestの登壇で火がつき、
今年のスケジュールは、このCakeFestを中心に進めることにしました。

ネタの準備

さすがにこの時までにCakePHP3移行は終わらないですが、
移行までの道筋を提示することはできるだろうと見積もっていました。

ただ、その前にやらなければいけないことが残っていました。
バージョンアップを優先し、着手できていなかったタスクです。

・スロークエリ潰し(ここ2年で増えたもの)
・サムネイル生成のリソース問題の対応

※その成果が、PHPカンファレンス沖縄の登壇内容と、このブログに反映されています

それぞれ、約1ヶ月ほど費やしましたので、
本格的に、CakePHP3化に着手できたのは8月下旬からでした。

※その成果が、以下のブログになります。

CakePHP3バージョンアップの開始
ModelのPHP3移行

登壇スライドは、↑に加えて進めていた内容も盛り込んでいます。
今後ブログにしていきたいと思います。

英語の準備

英語での発表は大きなプレッシャーでしたし、それなりに準備が必要だと感じました。
普段、Mailやチャットなどで英語を使うことはあり、それは落ち着いて翻訳できるのですが、
日常会話は即時性が求められます。

登壇については、カンペを作っておけば最悪なんとかなるだろうと思っていましたが、
質疑応答や、日常会話はできておく必要があると思い、勉強を始めました。

以下、私が主に準備した内容を紹介します。

オンライン英会話

オンライン英会話サービスは今回初めてやってみたのですが、非常に良いサービスだと思いました。
わざわざ教室に行かなくても良いし、費用もリーズナブルでPCで行えるというのも逆に好都合でした。
(Google翻訳を使ったりメモしたりが楽)

少し前までは、Skypeを使って行うことが多かったみたいですが、
最近はどのサービスも自前でビデオ通話システムを用意しており、
スマートフォン用のアプリも充実しているようです。(私はPCでのみやりました)

価格.comの比較ページを参考にし、

無料体験も含めて、以下のオンライン英会話サービスを利用してみました。

Kimini

https://glats.co.jp/

7日間無料(1日1回)
7日を過ぎると自動引き落としになります。

学研運営のサービス。
初めて利用したサービスです。

初心者向けのコースが充実していたので最初にこのサービスを試してみました。

いざ最初の一歩目は緊張していましたが、実際やってみると結構楽しいです。
講師はフィリピン人の方がほとんどだったと思います。

クラウティ

https://www.cloudt.jp/

14日間無料(1日1回)

これも学研運営です。
学研はオンライン英会話に力を入れているみたいですね。

14日間も体験できること自体が素晴らしいです。
個人的には電話応対のテキストが参考になりました。
(本番でそのシチュエーションはなさそうですが、今後役に立ちそう)

DMM英会話

https://eikaiwa.dmm.com/

とても評価の高いサービス。大体の方がまず薦めてくれます。
テキストが豊富で、講師陣の国籍も豊富。
(ネイティブスピーカーは体験時は選択できませんでした)

24時間レッスンできるのも良かったです。
夜中にレッスンを受けることが多かったので、セルビアの講師とよく話していました。

レアジョブ

https://www.rarejob.com/

こちらも評価高いです。
最初のステップで英会話レベルを判定してもらい、
カウンセリングを受けて、どのテキスト、レベルから始めたら良いかをアドバイスしてもらえます。
歴史も長いようで、英語教育そのものに本気で力入れている感が伝わってきます。

講師はフィリピン大学の学生がメインですが、良い講師が多くフィードバックも丁寧で早いです。
学部がわかるので、情報系の方の講師を中心にレッスンしてもらいました。

Native Camp

https://nativecamp.net/

無料枠のみの利用でしたが、7日間の間、1日に何回でも受けられるのが良かったです。
テキストは簡易的なものが多かったですが、予約したら即レッスン開始可能なシステムになっていて、
とにかく数をこなしたいなら一番良い選択肢かと思います。

過去のCakeFest動画を見る

当日の雰囲気を把握するために、過去のCakeFestの動画を見ました。
2017年開催のはほぼすべて確認しました。

例えばこの方。

冒頭で「Hey, guys!」との挨拶しています。
カジュアルさが伝わってきますが、私はとてもそんな挨拶はできないので
「Hello, everyone」にしておきました。

※gender問題もあるので「Hey, guys」は避けた方が無難と教えてもらいました

Core DeveloperのMark Storyさんの動画です。

CakePHP3へのアップグレードは「brutal」だと言っていますね。
「残忍だ」という表現。
私が訳すとdifficultになるのですが、確かに難しいというよりは残忍という表現は尖ってますけど的を射ていると思います。

私の発表でもこの「brutal」という言葉を1回だけ使わせてもらいました。

スライドの翻訳と逆翻訳

スライドはすべてカンペを英語、日本語両方用意しておきました。
(PowerPointのノート内に記載)

翻訳は主にGoogle翻訳で行いました。
この過程で気づいたことは、そもそも日本語を明確に書かないと
正しい英語に翻訳されないことです。

日本語は曖昧な表現でも聞き手がそれなりに解釈して伝わりますが、
曖昧なまま英語に機械翻訳すると、明らかに変な翻訳になることがあります。
翻訳の精度を高めるためには、まず明確な日本語での記述が必要になります。

Google翻訳で日本語から英語にした後に、違和感のある表現を直し、
そして逆に日本語に翻訳する、ような作業を繰り返し、確実な英文を作っていきました。

※この過程で覚えた語彙もたくさんあり、それが当日の日常会話で使えたりもしました。

社内リハーサル

リハーサルは計3回行いました。

まず、社内で英語が堪能な方に聞いてもらいました。
初めて言葉にして話して、猛烈な違和感を感じながらのプレゼンでした。
普段と違うことをしているので、これは慣れておくべきですね。

次に、社内開発部の方たちの前でリハーサルです。
※後で日本語でも話しました。

このときに出た質問の内容自体が難しかったことを覚えています。

自分の発表するテーマ自体、ツッコミやすいテーマであることは容易に想像できたし、
質疑応答は、日本語でもわからないのに、英語で来たらもう無理だなとはっきり自覚した瞬間でした(笑

Workshopへの参加

弊社のインターン生がWorkShopへの参加に意欲的だったので、
会社にお願いして、私も含めて参加させてもらいました。

Conference前にWorkShopに参加し、当日の雰囲気を少しでもつかんでおきたいと思いました。
結果、WorkShopに参加できたのは非常に良かったと思います。
CakeDCやCakePHPのコアデベロッパーの方々と簡単ですが話すことができ、当日の緊張を少しでも和らげることができたと思います。

また、WorkShop終了後に最後のリハーサルを行うこともできました。

最後のリハーサル

Workshop後に前夜祭があったのですが、開始まで1時間ほど時間がありましたので、
Workshopに参加していた、英語堪能な@mykhrdさんと日本人コミッターの@chimpeiさんに明日のプレゼンを見ていただけることになりました。

この際に以下のアドバイスをいただきました。

・できるだけ優しい言葉を使う。その方が伝わりやすい
 ※政治の演説や会社のビジョンが良い例
  ・「Just do It」とか「Yes, we can」など
・However, moreover等の前置詞、接続詞を言った後はひと呼吸おく

※リハーサルにWorkshopの会場を使わせていただいたDMMさんにも感謝です!

当日の同時通訳

当初、日本語で通訳できる人が見つかるかわからないという話でしたので、
英語での登壇前提で進めてきたのですが、CakeFestの1週間ほど前に、
日本語→英語の通訳が可能だという連絡を頂きました。

すでに英語、日本語の両方でカンペを作っていたので、
英語で発表し、通訳の方には日本語カンペを読んでいただければと思っていたのですが、
英語→日本語の通訳はなさそうな雰囲気でした。

が、当日になって日本語の同時通訳が行われることがわかりました。

プレゼンの時間は質疑応答を含めて35分です。
英語→日本語の通訳も想定して、リハーサルではプレゼン時間を20分くらいに収めていたので
特に内容は削らずに話そうと決めました。

※結局2倍の時間がかかり、一部飛ばしましたが。

通訳関連の決定は当日まで結構バタバタしましたが、
当日は、日本人も多数参加していたので、同時通訳があって良かったです。
(質疑応答の心配もいらなくなりましたし)

日本人トップバッターとして

発表順は数か月前に知らされていたのですが、私の発表順は3番目になりました。
日本人トップバッターとなり、他の方の発表の様子を見ながら軌道修正するということはできず。
しかも前の2つのセッションがCakePHPのコアデベロッパーの発表であったこともあり、
かなりプレッシャーになりました。

しかし、ポジティブに考えれば、日本人トップバッターという立場を利用して
以下のことを伝えることができると思いました。

・CakeFestが日本で開催されてうれしかったこと
・CakePHPは日本でとても人気があること
・最近はCakePHPの人気にも陰りが見えていること
・CakePHP3へのバージョンアップができずに、多くのCakePHP2のサービスが未だに動いている現状

終わってみて、この順番で発表できたことは非常に恵まれたと思いました。

最後に

Cakefestの会場は日本でしたが、その会場内は海外の雰囲気で、非日常感を味わいながら過ごしました。
日本に居ながら、このような経験はなかなかできないと思うので本当に貴重な時間を過ごせたと思います。

あと、半年くらい英語漬けにしてたので、日常会話くらいは何とか伝えられるようになりました。
良い機会なのでこれからも英語の勉強は続けていきたいと思います。

ModelのCakePHP3移行

kanazawa|2019年10月15日
CakePHP

SREチームの金澤です。

前回、ランサーズのCakePHP2→3へのバージョンアップを開始した記事を書きました。

今回は、ModelのCakePHP3移行について書きたいと思います。

CakePHP2からCakePHP3を読み込む

CakePHP2とCakePHP3を共存する方法を前回のブログで紹介しました。

Modelの移行については、@kunitさんの記事

CakePHP2 から CakePHP3 ORMを使ってみる

がとても参考になりました。

この方法をベースにModelのCakePHP3移行にトライしてみました。

まずは、cake28/Config/bootstrap.php に以下の設定を追加します。

define('CONFIG_CAKEPHP3', __DIR__ . '/../../config/');

if (!env('DB_MASTER_HOST') && file_exists(CONFIG_CAKEPHP3 . '.env')) {
    $dotenv = new \josegonzalez\Dotenv\Loader([CONFIG_CAKEPHP3 . '.env']);
    $dotenv->parse()
        ->putenv()
        ->toEnv()
        ->toServer();
}

try {
    \Cake\Core\Configure::config(
        'default',
        new \Cake\Core\Configure\Engine\PhpConfig(CONFIG_CAKEPHP3)
    );
    \Cake\Core\Configure::load('app', 'default', false);
} catch (\Exception $e) {
    exit($e->getMessage() . "\n");
}

\Cake\Datasource\ConnectionManager::setConfig(
    'default',
    \Cake\Core\Configure::read('Datasources.default')
);
\Cake\Datasource\ConnectionManager::setConfig(
    'slave',
    \Cake\Core\Configure::read('Datasources.slave')
);
\Cake\Datasource\ConnectionManager::setConfig(
    'test',
    \Cake\Core\Configure::read('Datasources.test')
);

\Cake\Cache\Cache::setConfig(
    '_cake_model_',
    \Cake\Core\Configure::read('Cache._cake_model_')
);

\Cake\I18n\I18n::setLocale(\Cake\Core\Configure::read('App.defaultLocale'));

そして、CakePHP2のUT実行時にCakePHP3側の処理もテスト用DBにアクセスするようにします。

これは、@okinakaさんの記事

CakePHP2からCakePHP3 ORMを使ったテスト

を参考にさせていただきました。

cake28/Test/bootstrap.php に以下の設定を追加します。

\Cake\Datasource\ConnectionManager::alias('test', 'default');
\Cake\Datasource\ConnectionManager::alias('test', 'slave');

test.phpで実行したときのパターンも想定し、
cake28/Config/bootstrap.php にも以下の設定を追加しておきます。

if (preg_match("|/test.php$|", $_SERVER['SCRIPT_FILENAME'])) {
    \Cake\Datasource\ConnectionManager::alias('test', 'default');
    \Cake\Datasource\ConnectionManager::alias('test', 'slave');
}

CakePHP3 ORM変換の挫折

続いて、CakePHP2からCakePHP3 ORMを呼び出す処理を実装します。

結論を言うと、この試みは挫折しました。

@kunitさんの記事を参考にさせていただき、加えて
既存のCakePHP2のControllerのソースを変更せずにModelのみCakePHP3に移行したかったため、
CakePHP2のfind関数をオーバーライドし、そこからCakePHP3のORMを呼び出して変換する方針で
実装していました。

現在のランサーズのソースコードで利用されているパターンを全て網羅するためには
例えば以下のパターンを考慮する必要があります。

  • InflectorでTable名→Model名変換
  • CakePHP3のEntityをCakePHP2の配列に変換
  • CakePHP3のDateTime型をフォーマット変換
  • find(‘list’)、find(‘all’)の対応
  • ORDER BYの対応

↑ここまでは対応していたのですが、

  • GROUP BYの対応
  • fieledのワイルドカード
  • バーチャルフィールドなしの集計関数 
  • HAVING区
  • CASE WHENなどの分岐
  • その他いろいろ

↑ここら辺も対応する必要があることがわかってきました。

ランサーズ内に出てくる、これらすべてのクエリを全てを網羅した変換処理を書き続けるよりも、
別なアプローチの方が良いと思いました。

※この挫折の詳細については、別途Qiitaに書きたいと思います。

CakePHP2からCakePHP3のModel関数を呼び出す

CakePHP3のORMをCakePHP2に変換するのではなく、
CakePHP2のModelの関数をCakePHP3に移行する方針に変更しました。

citiesテーブルにアクセスするCity ModelをCakePHP3に移行してみます。

<?php
class City extends AppModel
{
    public $name = 'City';
    public $validate = [
        'id' => ['numeric'],
    ];

    /**
     * 市区町村名を返す
     *
     * @param int|null $cityId 市区町村ID
     * @return string|null 市区町村名
     * @access public
     */
    public function getName($cityId)
    {
        $prefectural = $this->findById($prefecturalId);
        return isset($prefectural['Prefectural']['name']) ? $prefectural['Prefectural']['name'] : null;
    }
}

bakeでCakePHP3のModelを生成

CakePHP3のソースをbakeで生成します。

CakePHP3のbakeコマンドを実行すると

$ bin/cake bake model Cities

以下のファイルが生成されます。
src/Model/Entity/City.php
src/Model/Table/CitiesTable.php
tests/Fixture/CitesFixture.php
tests/TestCase/Model/CitiesTableTest.php

CakePHP3のTableにCakePHP2の関数を移植

CakePHP2のgetName関数をCakePHP3のTableに移植します。

src/Model/Table/CitiesTable.php


    public function __construct($id = false, $table = null, $ds = null)
    {
        parent::__construct($id, $table, $ds);
        $this->cake3Table = Cake\ORM\TableRegistry::getTableLocator()->get('Cities');
    }

    /**
     * 市区町村名を返す
     *
     * @param int|null $cityId 市区町村ID
     * @return string|null 市区町村名
     * @access public
     */
    public function getName(?int $cityId) : ?string
    {
        if (!isset($cityId)) {
            return null;
        }
        $result = $this->find()->select(['name'])->where(['id' => $cityId])->first();
        return isset($result) ? $result->name : null;
    }

CakePHP2のModelからCakePHP3の関数を呼び出す

cake28/Model/City.php

    /**
     * 市区町村名を返す
     *
     * @param int|null $cityId 市区町村ID
     * @return string|null 市区町村名
     * @access public
     */
    public function getName($cityId)
    {
        return $this->cake3Table->getName($cityId);
    }

この状態で、CakePHP2のUTが正常に終了すれば移行完了になります。

Validationの移行について

この手順で移行すれば、
CakePHP2のController

CakePHP2のModel

CakePHP3のTable
の順番で呼び出されるため、一応、ModelのみCakePHP3に移行できます。

そして、ControllerをCakePHP3に移行したタイミングで
CakePHP3のModelが直接呼び出されるようになっても
CakePHP3に実装した関数は同様に動作するはずです。

しかし、Validationについては、まだCakePHP2のModelで動作している状態です。

CakePHP3のValidationは、CakePHP2から大幅に変更されています。
Validationクラスやモデルのないフォーム等、
新しいValidationの仕組みに対応する必要があるため、
単純に関数を移行すればOKというわけではありません。

Validation絡みの処理に関しては、bakeで生成されるもの以外は
Controller移行時にまとめて行うことにしました。

CakePHP3用のUT実行

現状、UTはCakePHP2側に寄せていますが、
CakePHP3のbakeで生成したValidationについても、CakePHP2との互換性を確認しておきたいところです。
そのため、CakePHP3のValidationの確認をCakePHP3のUTで行うことにしました。

現状、CakePHP2のUT実行は以下のコマンドで行っています。
(phpunit.xmlでcake28/Test/bootstrap.phpを読み込んでいる)

$ ./cake28/Console/cake l_test cake28/Test/Case/Model/CityTest.php

CakePHP3のUTは以下のコマンドで実行できます。
(CakePHP3のbootstrap.phpを読み込んでphpunitを実行)

$ ./bin/phpunit --bootstrap config/bootstrap.php tests/TestCase/Model/Table/CitiesTableTest.php

CakePHP3のValidationのテストは以下のように実装しました。
(getErrorを呼び出し、内容を確認)

tests/TestCase/Model/Table/CitiesTableTest.php

    public function testValidate()
    {
        // 正常系
        $city = $this->Cities->newEntity([
            'id' => 1,
            'prefectural_id' => 1,
            'name' => '札幌市',
            'created' => '2019-09-04 12:27:03',
            'modified' => '2019-09-04 12:27:03'
        ]);
        $this->assertSame([], $city->getErrors());

        // nameが空
        $city = $this->Cities->newEntity([
            'id' => 1,
            'prefectural_id' => 1,
            'name' => '',
            'created' => '2019-09-04 12:27:03',
            'modified' => '2019-09-04 12:27:03'
        ]);
        $this->assertSame(["name" => ["_empty" => "入力してください", ]], $city->getErrors());
    }

今後の予定

まずは一部の機能に絞って、Model、Lib、Controller、viewを一通り移行したいと考えています。

今回の方針も含め、まだ手探りな部分が多いため、
一通りModel、Lib、Controller、viewのCakePHP3移行を行い、この手法が有効かどうかを検証し、
問題なさそうであれば、本格的にすべてのModel移行を開始したいと思います。

CakeFest2019に登壇します

2019/11/7 – 2019/11/10にCakeFestが東京で開催されます。

CakeFestはCakePHPの世界的なイベントで、2019年は日本で開催されることになりました。
※ランサーズは前回の2017年に続き、2019年もスポンサーになっています。

「CakePHP3への滑らかな移行を考える」というテーマが採択されました。
本記事の内容も踏まえて発表する予定です。

CakePHP2→3バージョンアップの開始

kanazawa|2019年09月02日
CakePHP

SREチームの金澤です。

ランサーズのCakePHP2→3へのバージョンアップを開始しました。

これから何回かに分けてお話したいと思います。

第1回目は、バージョンアップ方針と
CakePHP新旧バージョンの共存方法についてお話したいと思います。

プロダクト規模

ランサーズのプロダクト規模は概ね以下のようになります。
この規模のソースを、普段の開発を止めずにバージョンアップする必要があります。

ディレクトリ ファイル数 ステップ数
Config 約190 約60000
Console 約170 約30000
Controller 約350 約110000
Lib 約1100 約153000
Model 約420 約105000
View 約2800 約240000
合計 約5030 約698000

CakePHP1.3→2.8の段階的バージョンアップ

そのため、CakePHP1.3→2.8移行は段階的に、controller単位で、1年以上かけて行いました。

その内容について、2018年のPHPカンファレンスでも発表させていただきました。

移行中のディレクトリ構成は以下のようになっていました。

+ app(※CakePHP1.3のコード)
 + config
 + controller
 + lib
 + model
+ cake
 + ※CakePHP1.3本体
+ cake28(※CakePHP2.8のコード)
 + Config
 + Controller
 + Lib
 + Model
+ vendor
 + cakephp
  + ※CakePHP2.8本体

この方法の欠点は、
Controllerに焦点を当て、初めにModel、Libをコピーする形をとったため、
普段の開発で両バージョンのModel、Libを修正しなければならなくなったことです。

CakePHP3ではModelが大幅に変更されていることもあり、同じアプローチでは取れません。
そもそも、両バージョンに修正行う構成は避けたいところです。

CakePHPの処理の流れの概要は以下のようになります。

普段の開発で、両バージョンの修正が必要な状態にならないようにするためには、
原則、矢印の終端のコードから移行していく必要があります。

CakePHP2とCakePHP3の共存

まず、CakePHP本体の共存から開始します。

CakePHP1.3→2.8移行のときは、CakePHP1.3をcakeディレクトリに配置し、
CakePHP2.8はvendorディレクトリでcomposer管理していました。

CakePHP2とCakePHP3を共存させるために、CakePHP3をcomposer管理します。

CakePHP2をcake28/Vendor/cakephp に移動します。
CakePHP2本体そのものに手をいれることを想定し、git管理します。

+ cake28(※CakePHP2のコード)
 + Config
 + Controller
 + Lib
 + Model
 + Vendor
  + cakephp
   + ※CakePHP2本体
+ vendor
 + cakephp
  + ※CakePHP3本体

加えて、config、srcディレクトリを作成し、ここにCakePHP3のコードを書いていきます。

+ config(※CakePHP3のコード)
 + .env
 + App.php
+ src(※CakePHP3のコード)
 + Controller
 + Lib
 + Model
+ cake28(※CakePHP2のコード)
 + Config
 + Controller
 + Lib
 + Model
 + Vendor
  + cakephp
   + ※CakePHP2本体
+ vendor
 + cakephp
  + ※CakePHP3本体

旧バージョンでの開発抑止

CakePHP1.3→2.8のバージョンアップが長引いた要因の一つに、
普段の開発がCakePHP1.3で進められていたことがあります。

バージョンアップの進捗状況を壁にも書いていたのですが、途中で棚卸をした段階で、
多くの新規コントローラーがCakePHP1.3に追加されていたことが判明しました。

フレームワークのバージョンアップは長丁場になります。
その間にも普段の新規開発は行われるため、
なるべくこれらを新バージョンで開発してもらう工夫が必要になります。

CIで旧バージョンの新規開発をチェックする

バージョンアップを行うにあたり、CIで文法チェックを行っていましたが、これは非常に効果がありました。

※詳細は以前のブログに詳しく書いています。

ランサーズの開発は、CIのチェックを通らないとマージできないように設定しています。

新規開発を新バージョンで進めてもらうために、CIのチェックに
CakePHP旧バージョンでの開発を抑止するチェックを入れる予定です。

CakePHP2→4バージョンアップについて

この単位で段階的にバージョンアップするのであれば、
CakePHP2→4へのバージョンアップもできるかも知れないと思い、検討しました。

しかし、CakePHP4では、要求するPHPUnitの最低バージョンが8.3.3以上になります。
CakePHP2では、PHPUnit6以上になるとUTが正しく動作しなくなります。

$ ./cake28/Console/cake l_test cake28/Test/Case/Model/CityTest.php
Warning Error: include(PHPUnit/Autoload.php): failed to open stream: No such file or directory in [/var/www/lancers/cake28/Vendor/cakephp/cakephp/lib/Cake/TestSuite/CakeTestSuiteDispatcher.php, line 178]

Warning Error: include(): Failed opening 'PHPUnit/Autoload.php' for inclusion (include_path='/var/www/lancers/vendor/phpunit/phpunit:/var/www/lancers/cake28/Vendor/cakephp/cakephp/lib:/var/www/lancers/cake28/Vendor/cakephp/cakephp/lib:.:/usr/share/pear:/usr/share/php') in [/var/www/lancers/cake28/Vendor/cakephp/cakephp/lib/Cake/TestSuite/CakeTestSuiteDispatcher.php, line 178]

Error: Please install PHPUnit framework v3.7 (http://www.phpunit.de)
#0 /var/www/lancers/cake28/Vendor/cakephp/cakephp/lib/Cake/Console/ShellDispatcher.php(221): TestShell->initialize()
#1 /var/www/lancers/cake28/Vendor/cakephp/cakephp/lib/Cake/Console/ShellDispatcher.php(66): ShellDispatcher->dispatch()
#2 /var/www/lancers/cake28/Console/cake.php(56): ShellDispatcher::run(Array)
#3 {main}
$

CakePHP2のUTを利用しながらバージョンアップを行いたいので、現状はCakePHP3に留めました。
CakePHP4へのバージョンアップは、UTをCakePHP3ベースに切り替え、
PHPUnitを8.3.3以上にバージョンアップできたタイミングで検討したいと思います。

※2019/10/18追記 CakePHP3でも以下のエラーとなりました。
CakePHP3本体をPHPUnit8.3.3に対応させる必要がありますが、戻り値の型宣言が必要なのでPHP7が必須になりそう。

$ ./bin/phpunit --bootstrap config/bootstrap.php tests/TestCase/Model/Table/CitiesTableTest.php
PHP Fatal error:  Declaration of Cake\TestSuite\TestCase::setUp() must be compatible with PHPUnit\Framework\TestCase::setUp(): void in /var/www/lancers/vendor/cakephp/cakephp/src/TestSuite/TestCase.php on line 35
PHP Stack trace:
PHP   1. {main}() /var/www/lancers/vendor/phpunit/phpunit/phpunit:0
PHP   2. PHPUnit\TextUI\Command::main() /var/www/lancers/vendor/phpunit/phpunit/phpunit:61
PHP   3. PHPUnit\TextUI\Command->run() /var/www/lancers/vendor/phpunit/phpunit/src/TextUI/Command.php:159
PHP   4. PHPUnit\TextUI\TestRunner->getTest() /var/www/lancers/vendor/phpunit/phpunit/src/TextUI/Command.php:177
PHP   5. PHPUnit\TextUI\TestRunner->loadSuiteClass() /var/www/lancers/vendor/phpunit/phpunit/src/Runner/BaseTestRunner.php:101
PHP   6. PHPUnit\Runner\StandardTestSuiteLoader->load() /var/www/lancers/vendor/phpunit/phpunit/src/Runner/BaseTestRunner.php:141
PHP   7. PHPUnit\Util\FileLoader::checkAndLoad() /var/www/lancers/vendor/phpunit/phpunit/src/Runner/StandardTestSuiteLoader.php:36
PHP   8. PHPUnit\Util\FileLoader::load() /var/www/lancers/vendor/phpunit/phpunit/src/Util/FileLoader.php:47
PHP   9. include_once() /var/www/lancers/vendor/phpunit/phpunit/src/Util/FileLoader.php:59
PHP  10. spl_autoload_call() /var/www/lancers/tests/TestCase/Model/Table/CitiesTableTest.php:11
PHP  11. Composer\Autoload\ClassLoader->loadClass() /var/www/lancers/tests/TestCase/Model/Table/CitiesTableTest.php:11
PHP  12. Composer\Autoload\includeFile() /var/www/lancers/vendor/composer/ClassLoader.php:322
PHP  13. include() /var/www/lancers/vendor/composer/ClassLoader.php:444
$

今後の予定

次回は、最大の難関であるModelのバージョンアップについて書きたいと思います。

CakeFest2019に登壇します

2019/11/7 – 2019/11/10にCakeFestが東京で開催されます。

CakeFestはCakePHPの世界的なイベントで、2019年は日本で開催されることになりました。
※ランサーズは前回の2017年に続き、2019年もスポンサーになっています。

「CakePHP3への滑らかな移行を考える」というテーマが採択されました。
本記事の内容も踏まえて発表する予定です。

API GatewayとLambdaとGolangで作るサムネイル生成システム

kanazawa|2019年08月26日
API Gateway

SREチームの金澤です。

ランサーズのサムネイル生成をAPI Gateway + Lambdaのシステムにリニューアルしました。
今回、その内容について書きたいと思います。

以前のサムネイル生成処理

今までのサムネイル生成処理は、Appサーバー内でImageMagickを起動して行っていました。
img.lancers.jpのURLにアクセスしたタイミングで以下の処理を実行していました。

  • S3から元画像をダウンロード
  • ImageMagickのconvertコマンドでサムネイルを生成して表示
  • その結果をCloudFrontにキャッシュ

※これらの処理については以前のブログに詳しく書いています。

以前のサムネイル生成処理が引き起こしていた問題

このImageMagickによるサムネイル生成処理は、サービスリリース直後に実装されたもので、ほとんど手が入らないまま10年以上運用されていました。

しかし、近年、以下の問題が目立つようになってきていました。

メモリ、ストレージの逼迫問題

アップロードのファイルサイズ制限が緩和され、巨大な画像がアップロードされることが多くなりました。
その結果、Appサーバーのメモリ、ストレージが逼迫し、それが原因でサーバーが落ちることも多くなりました。

ImageMagickは大きな画像になるとメモリやストレージを大幅に消費します。
policy.xmlの設定次第では、メモリを1GB以上消費します。
また、変換時に/tmp/に生成されるファイルが数GBになることもあります。

一度重い変換処理が走ると、画像が変換に時間がかかり、その間に再び同じ処理が発生します。
これが繰り返されることで、プロセスが溜まりAppサーバーのストレージが逼迫することが多くなりました。

セキュリティ問題

ImageMagickは脆弱性が数多く報告されており、その中には極めて深刻な脆弱性も含まれています。
Appサーバーをその脆弱性から守るためには、常に最新バージョンに保ち、policy.xmlをメンテナンスする必要がありますが、またいつ脆弱性が報告されてもおかしくない状態です。

安定性の問題

同じ画像を一度に複数サムネイル化すると、高い確率でサムネイル化に失敗する現象が起きていました。

例えば、ユーザープロフィールの画像を更新した場合、以下の3か所が全て更新されるはずが、1つ以上失敗してしまう現象が発生していました。

原因は完全には突き止められませんでしたが、S3からダウンロードされる元画像が同じために上書きが発生し、それが問題を引き起こしていた可能性が高いです。

API Gateway + Lambda によるサムネイル生成

これらの問題を解決するためにサムネイル生成システムのリニューアルにとりかかりました。
API Gateway + Lambdaを採用した理由は以下になります。

サーバー分離によるセキュリティの確保

サムネイル生成処理をAppサーバーと分離することで、サムネイル生成に乗じた攻撃に関して、Appサーバーへ影響を回避できます。

そもそも、サムネイル処理は専用のサーバーで行うべきという意見は以前からありました。
しかしながら、専用サーバーを構築するコスト、冗長性の確保を含むサーバーコスト、メンテナンスコストなどの増大の懸念もあり、対策が後回しになっていました。

Lambdaでサーバーレス化することで、これらのコスト問題をクリアできる見込みができました。

また、サムネイル生成処理がlambdaのコンテナ単位、ms単位での処理となることで、仮に脆弱性を突かれてもその影響を最小限に食い止めることができます。

メモリ、ストレージ逼迫問題の解決

サムネイル処理がAppサーバーから分離され、Appサーバーのメモリ、ストレージ逼迫問題が解決します。
また、Lambdaではコンテナ単位で実行されるため、複数実行されても独立してメモリ、ストレージが確保されます。

安定、確実な生成処理

Lambdaではそれぞれの処理が隔離されたコンテナで実行されるため、S3のダウンロード時にファイルが上書きされるようなことは起こらなくなります。
処理が各コンテナで閉じられるため、安定した処理が期待できました。

Golangを選択した理由

LambdaのランタイムにはGolangを採用しました。
理由は以下になります。

ストレージの制限

Lambda関数を起動するコンテナにはImageMagickがインストールされています。
そのため、ImageMagickを利用する選択もありました。

Lambdaには以下の制限があります。
https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/limits.html

特に、/tmpディレクトリが512MBまでしか使えないという点が厳しく、ImageMagickだと今回解決させたい問題の1つである、ストレージ逼迫問題が再発する可能性がありました。

そのため、サムネイル処理をGolangのimageパッケージで行うことにしました。

※後述しますが、一部の処理ではImageMagickを利用しています。

高速な処理

Lambdaは100ms単位の課金のため、費用面からもできるだけ処理は早く終了させたいところです。
できるだけ100ms以内に終了させたく、処理速度が高速なGolangに期待しました。

構築時のTips

以下、構築時に対応したポイントをいくつか紹介します。

Lambda

画像を返すために必要な設定

Base64で返す必要があります。

    return events.APIGatewayProxyResponse{
        StatusCode:      200,
        Body:            string(getBase64("/tmp/" + s3FileInfo.GetFileName())),
        IsBase64Encoded: true,
        Headers: map[string]string{
            "Content-Type": "image/jpeg",
        },
    }, nil

画像をBase64で返す関数は以下のように実装しました。

func getBase64(srcFileName string) string {
    file, err := os.Open(srcFileName)
    if err != nil {
        err = errors.Wrap(err, "Failed to os.Open")
        panic(err)
    }
    defer file.Close()

    fi, err := file.Stat()
    if err != nil {
        err = errors.Wrap(err, "Failed to file.Stat")
        panic(err)
    }

    size := fi.Size()
    data := make([]byte, size)
    b, err := file.Read(data)
    if err != nil {
        fmt.Println(string(b))
        err = errors.Wrap(err, "Failed to file.Read")
        panic(err)
    }

    return base64.StdEncoding.EncodeToString(data)
}

ImageMagickとの併用

サムネイル生成処理の99%以上はGolangのImageパッケージで行っていますが、以下の場合に限り、ImageMagickでjpgに変換する処理を行っています。

  • Golangのimageパッケージが対応していない画像のとき
  • Golangのimageパッケージの処理が失敗したとき

※Lambda関数のコンテナ内でも、convertコマンドを直接起動できます。
以下、実装例です。

cmd := exec.Command("convert", "-background", "white", "-flatten", srcFileName, dstFileName)
err := cmd.Start()
...
err := cmd.Wait()

API Gateway

jpegを返すための設定

バイナリメディアタイプにimage/jpegを追加する必要があります。

CloudFront

jpegを返すための設定

Origin Custom Headersに以下の設定を追加する必要があります。

サーバー費用の見積りとメモリ設定

Lambdaの料金は以下に記載されています。
https://aws.amazon.com/jp/lambda/pricing/

月間1000万回、すべて100ms以内に処理が終了したと仮定して概算してみました。
(※1$=120円で計算)

メモリ 100ms単位の価格 1000万回の価格 平均処理時間
128MB 約0.0000250円 約250円 約1800ms
256MB 約0.0000500円 約500円 約800ms
3008MB 約0.0005000円 約5875円 約400ms

巨大画像の処理を想定すると、メモリはなるべく多めに確保しておきたいところです。
また、Lambdaのメモリを増やすと、終了時間も早くなるため、単純に費用は比例せず、ある程度削減されます。
メモリ3008MBに設定したときは、半分以上の処理が100msを切るようになりました。

最大に設定しても大幅に費用が跳ね上がることはないと判断し、Lambdaのメモリは最大の3008MBに設定しました。
(タイムアウト値は20秒に設定)

結果

巨大画像の処理時間とメモリ使用量

15917 x 16344の画像で検証してみました。

この画像はImageMagickで変換すると10秒以上かかり、CPU、メモリを大きく消費していました。
また、/tmp/ ディレクトリに数GBのファイルを作成します。
そのため、複数プロセス同時に起動すると処理が返らずに暴走し、Appサーバーを不安定にさせる要因になっていました。

新システムでは、メモリ使用量1313MB、処理時間約1100msでサムネイル化することができました。
処理時間はほぼ同じですが、新システムは、Appサーバーから分離されたため不安定にさせる要因がなくなりました。
また、Lambdaでコンテナ単位で処理が隔離されたため、複数同時に起動しても確実に生成できるようになりました。

Lambda起動数、平均処理時間、成功率

以下は1週間分の計測結果です。

平均処理時間は約220msくらいです。
成功率は概ね99.9%以上ですが、1日に数回頻度でタイムアウト値の20秒を超えてしまうものもありました。

最後に

API Gateway + Lambdaの仕組みに移行することで、サムネイル処理に伴う問題を一通り解決させることができました。

今後、失敗した画像を分析し、さらに成功率を高めていきたいです。
また、条件が合えばLambda@Edgeへの置き換えも検討していきたいと思います。

PHP5.6→7.3移行が完了しました

kanazawa|2019年05月30日
CakePHP

SREチームの金澤です。

PHP5.6→7.3バージョンアップが完了しました。
PHP5.3→5.6バージョンアップが完了してから約2カ月での移行となりました。

今回、その対応内容と結果を報告したいと思います。

バージョンアップ準備

PHP7化については、有用な記事が数多くありましたので、まずはそれらを参考にさせていただきました。

CakePHP2.10化

PHP5.6化後のライブラリアップデートのタイミングでCakePHP 2.8から2.10にバージョンアップしました。

CakePHP2.9のタイミングでObjectクラスが非推奨になったため、CakeObjectに名前変更しました。
※PHP7ではObjectが予約語になります。

廃止、非推奨となる関数の対応

対応が必要だったのは主に以下の関数です。

  • __autoload
  • ereg_*
  • eregi
  • each
  • mysql_*
  • split

※対応詳細はGithubにまとめました。
これらの関数は、幸いにもそれほど対応は難しくありませんでした。

mcrypt対応

今回、ここに最も時間がかかりました。

PHP7.2からmcryptはコアから削除されるため、使えなくなります。
PECLのmcryptを入れるという回避策もありましたが、この機会にすべてOpenSSLに移行しました。

※こちらも、詳細をGithubにまとめています。

移行の際は、互換性を維持するためにUTを拡張し、OpenSSL移行前、移行後で暗号化された文字列そのものが一致することを確認しながら進めました。

PEAR::Crypt_Blowfish(Blowfish ECB)対応

PEAR::Crypt_Blowfishは内部でmcrypt関数を使用しており、PHP7.3では利用できません。

今回移行した箇所のアルゴリズムはBlowfishのECBモードで、IVを必要としないはずですが、暗号化処理の過程でなぜかIVを生成していたため少々混乱しました。

mcrypt関数はブロック暗号の処理の過程で文字列を固定ブロック長に分解しますが、
最後のブロックの余った部分に0x00パディングを行います。

OpenSSLでこの処理をするオプションが見当たらず苦労しました。

結局、以下のサンプルソースを参考にし、自前で0x00パディングを実装したところ、UTの差分が消えました。
https://www.php.net/manual/ja/function.openssl-encrypt.php

Blowfish CBC対応

mcrypt関数で直接BlowfishのCBCモードで暗号化を行っている箇所があったので、そこもOpenSSLに移行しました。
これも同様に0x00パディングの処理を自前で実装しています。

CakePHP対応

PHP7.3に切り替えた当初、CakePHP側でmcryptのエラーが出てしまい、動作しませんでした。

以下の設定を入れることで対応できます。

Configure::write('Security.useOpenSsl', true);

※当初、PECLのmcryptをインストールして回避しようとしていましたが、
@chimpeiさんに教えてもらい、解決することができました。
大変感謝しております。

※本件、英語ドキュメントには書かれていたのですが、日本語ドキュメントには書かれていていなかったため、追加するPRを出しておきました。

phpcc

事前準備で知っている範囲をすべて対応した上で、phpccでチェックを行いました。
(ctpファイルはチェックしないようなので注意が必要です)

このタイミングで、PHP4形式でコンストラクタを記述している箇所が見つかり、対応することができました。

PHP7.3コンテナの構築と切り替え

PHP5.6化のときと同様、開発用コンテナのPHP7.3化から始めました。

ランサーズの開発環境はDockerを利用しており、docker-compose.ymlには以下のように記述しております。

version: '2'

services:
  app:
    image: xxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/lancers_app:latest
    hostname: app
    networks:
      lancers:
        ipv4_address: 10.100.6.11
    extra_hosts:
       - "dev.lancers.jp:10.100.50.11"
…
    container_name: app-6-11
    volumes:
      - ~/www:/var/www

PHP5.6環境とPHP7.3環境をすぐに切り替えられるようにしました。
構築したPHP7.3のコンテナはタグ名を7.3とし、docker-compose.override.ymlを以下のように記述します。

version: '2'

services:
  app:
    image: xxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/lancers_app:7.3

PHP7.3で稼働させたい場合は、このdocker-compose.override.ymlを配置して

$ docker-compose up -d

とすれば、PHP7.3コンテナにすぐに切り替わります。

PHP5.6コンテナに戻したい場合は、imageの行を#でコメントアウトし、再度

$ docker-compose up -d

とすれば戻ります。

PHP7.3対応

検証環境を先んじてPHP7.3に移行し、動作確認を進めました。

PHP-FPMのエラーログを確認しながら非互換を調査。
このフェーズで、主に以下の対応を行いました。

  • static宣言していない関数のstatic呼び出し対応
  • 引数の数が一致しない関数呼び出しの対応
  • スコープの厳密化に伴う対応

PHP7.3化後のパフォーマンス

サーバーレスポンス

移行前に150ms前後であったものが90ms前後に改善。

PHP5.3→5.6のときは約20msほど改善しましたが、その3倍のパフォーマンスアップとなりました。

CPU使用率

CPU使用率はほぼ半減しました。
以下のグラフは1台のみPHP7.3に切り替えたタイミングですが、PHP5.6のサーバーが35%前後なのに対し、PHP7.3のサーバーは15%前後の使用率となっています。

さすがにサーバーを持て余すようになりました。
メモリにも余裕があったので、PHP-FPMのプロセス数はそのままで、サーバーをc5.2xlargeからc5.xlargeにスケールダウンして現在は運用しています。

今後の予定とお知らせ

バージョンアップフェーズについて

ランサーズのPHP、CakePHPバージョンアップは、以下のフェーズに分けて取り組んできました。

Apache + mod_php + PHP 5.3 + CakePHP 1.3
↓(第1フェーズ)約2か月
Nginx + PHP-FPM + PHP 5.3 + CakePHP 1.3
↓(第2フェーズ)約1年超
Nginx + PHP-FPM + PHP 5.3 + CakePHP 2.8
↓(第3フェーズ)約2か月
Nginx + PHP-FPM + PHP 5.6 + CakePHP 2.8
↓(第4フェーズ)約1日
Nginx + PHP-FPM + PHP 5.6 + CakePHP 2.10
↓(第5フェーズ)約2カ月
Nginx + PHP-FPM + PHP 7.3 + CakePHP 2.10

バージョンアップ決断時の
PHP 5.3 + CakePHP 1.3
という構成から、約2年かけて
PHP 7.3 + CakePHP 2.10
までバージョンアップすることができました。

PHP7化は、バージョンアップを決断したときの大きな目標でした。
今回は1つの大きな区切りとなります。

CakeFest2019に登壇します

2019/11/7 – 2019/11/10にCakeFestが東京で開催されます。

CakeFestはCakePHPの世界的なイベントで、2019年は日本で開催されることになりました。
※ランサーズは前回の2017年に続き、2019年もスポンサーになっています。

今回、提出したCFPのうち、「CakePHP3への滑らかな移行を考える」というテーマが採択されました。
CakePHP2から3へのアップグレードは世界的にも大きなテーマであると感じました。

ランサーズでは、CakePHP1.3から2へコントローラー単位で徐々に移行してきましたが、1年以上かかりました。
CakePHP2→3へのアップグレード難易度はそれより高く、かつ普段の開発を止めずに移行することが求められます。

まずはCakeFestに向け、CakePHP3へのアップグレード方法について試行錯誤をしていきたいと思います。

PHP5.6化後のライブラリアップデート

kanazawa|2019年04月19日
AWS

SREチームの金澤です。

PHP5.6にバージョンアップしたことにより、今までPHPバージョン依存でアップデートできなかったライブラリをアップデートできるようになりました。

今回は、そのアップデート内容についてお話したいと思います。

AWS PHP SDK V3

2019/6/24にAmazon S3のAWS署名バージョン2が廃止されます。

AWS署名バージョン4に移行が必要になるのですが、バージョン4にするためには、原則、AWS PHP SDK V3にアップデートする必要があります。

AWS PHP SDK V3はPHP5.5以上が必須であったためPHP5.6対応は時間との戦いでもありました。
PHP5.6アップデート完了後、速やかにV3にバージョンアップに取り掛かりました。

Composer1.7.3以降でAWS SDKがインストールできない問題

2018/11にComposer1.7.3がリリースされたのですが、このバージョン以降になると
AWS SDK V2がインストールできなくなります。(PHP5.3、PHP5.6で確認)

※この詳細はQiitaに書きました。

Composerバージョンを1.7.2のままでしばらく運用していたため、
AWS SDK V3へのバージョンアップもComposerバージョン1.7.2で行いました。

Guzzleのバージョンアップ

AWS SDK V2はGuzzle(バージョン3)に依存しているのに対し、
AWS SDK V3はGuzzleHttp(バージョン6)に依存しています。

AWS SDKをV3にバージョンアップすると、依存しているGuzzleもバージョンアップされるので同時に対応する必要があります。

※バージョンアップに伴うソース修正内容をGitHubリポジトリに公開しました。

Goutteのバージョンアップ

Guzzleに依存するGoutteのバージョンアップも同時に行う必要がありました。
V1.0→V3.1.0へのバージョンアップしました。

AWS SDKのバージョンアップ

composer.jsonを以下のように修正し、

  "require": {
-    "aws/aws-sdk-php": "2.*",
+    "aws/aws-sdk-php": "3.*",

composer updateを行うと、AWS SDKとGuzzleがバージョンアップされるはずなのですが、

$ php composer.phar update

ランサーズで利用しているcomposerライブラリが全般的に古く、依存関係が複雑なため
composer updateが進まない状態になっていました。

そのため、AWS SDKバージョンアップ時に生成されるcomposer.lockの内容を控え、
手動でcomposer.lockを書き換えて、composer installを行いました。

$ php composer.phar install

※こちらも、具体的なソース修正内容をGitHubリポジトリに公開しています。

Composerのバージョンアップ

AWS SDKとGuzzle HTTPを最新版にした結果、
最新のComposerでもcomposer updateができるようになりました。

composer self-updateをして、composer 1.7.2→1.8.5にアップデートできました。

$ php composer.phar self-update

PHP-CS-Fixer

v2.1から最新のv2.14にバージョンアップすることができました。
v2.1では設定できなかった以下の設定を有効にすることができました。

    ->setRules(array(
        '@PSR2' => true,
        '@PHP56Migration' => true,
-//        'cast_spaces' => array('space' => 'none'), // PHP CS Fixer 2.2.20 では設定不可
-//        'combine_consecutive_issets' => true, // PHP CS Fixer 2.2.20 では設定不可
+        'cast_spaces' => array('space' => 'none'),
+        'combine_consecutive_issets' => true,

facebook/graph-sdkのcomposer管理

facebook/graph-sdkについては、今までPHP5.3で動作するFork版のV4.0.23を、
Vendorディレクトリに直接配置していました。

まずはV4.0.23のまま、Composer管理に移すことから開始しました。

  "require": {
…
+    "facebook/graph-sdk": "4.0.23",

※Guzzle最新バージョンにすれば、最新のfacebook/graph-sdkを利用可能になります。
(ソース修正が必要)

PHPMailer

5.2.27→6.0.7へバージョンアップ。
バージョン5.2.27は__autoload関数が使われていますが、PHP7.2で非推奨になります。
バージョン6でこれらの対応が行われています。

※バージョン6以降は以下の記述で読み込むようになります。

use PHPMailer\PHPMailer\PHPMailer;

PHPUnit

PHP5.6では、PHPUnitを5.7までバージョンアップできるのですが、
バージョンアップして実行すると以下のエラーが出力されます。

[lancers@app lancers]$ ./cake28/Test/CodingChecker/cakephp/unittest.sh
/var/www/lancers/cake28/Test/Case/Config/IsPcTest.php
CakePHP Test Shell
---------------------------------------------------------------
PHP Strict Standards:  Declaration of CakeTestRunner::doRun() should be compatible with PHPUnit_TextUI_TestRunner::doRun(PHPUnit_Framework_Test $suite, array $arguments = Array, $exit = true) in /var/www/lancers/vendor/cakephp/cakephp/lib/Cake/TestSuite/CakeTestRunner.php on line 29
Strict Standards: Declaration of CakeTestRunner::doRun() should be compatible with PHPUnit_TextUI_TestRunner::doRun(PHPUnit_Framework_Test $suite, array $arguments = Array, $exit = true) in /var/www/lancers/vendor/cakephp/cakephp/lib/Cake/TestSuite/CakeTestRunner.php on line 29
PHP Fatal error:  Access level to CakeTestCase::expectException() must be public (as in class PHPUnit_Framework_TestCase) in /var/www/lancers/vendor/cakephp/cakephp/lib/Cake/TestSuite/CakeTestCase.php on line 748
Fatal error: Access level to CakeTestCase::expectException() must be public (as in class PHPUnit_Framework_TestCase) in /var/www/lancers/vendor/cakephp/cakephp/lib/Cake/TestSuite/CakeTestCase.php on line 748
Fatal Error Error: Access level to CakeTestCase::expectException() must be public (as in class PHPUnit_Framework_TestCase) in [/var/www/lancers/vendor/cakephp/cakephp/lib/Cake/TestSuite/CakeTestCase.php, line 748]

CakePHP2.8がPHPUnit5.7に対応していないためです。
CakePHP2.10は対応しているので、これはCakePHP2.10移行後の対応になります。

CakePHP2.10

ということで、CakePHP2.10にアップデートしました。

CakePHP2.8→CakePHP2.10に関しては特に大きな問題もなくアップデートできました。

CakePHP2.9のタイミングでObjectクラスが非推奨になりました。
(PHP7でObjectが予約語になっている)
https://book.cakephp.org/2.0/ja/appendices/2-9-migration-guide.html
そのため、アップデート後にObjectからCakeObjectへの置き換えを行いました。

その他のライブラリ

psysh

v0.8.14→v0.9.9へバージョンアップ

PHPExcel

phpoffice/phpexcelはサポートを終了しているので
phpoffice/phpspreadsheetへ移行。

今後の予定

ランサーズのPHP、CakePHPバージョンアップは、以下のフェーズに分けて取り組んでいます。

Apache + mod_php + PHP 5.3 + CakePHP 1.3
↓(第1フェーズ)
Nginx + PHP-FPM + PHP 5.3 + CakePHP 1.3
↓(第2フェーズ)
Nginx + PHP-FPM + PHP 5.3 + CakePHP 2.8
↓(第3フェーズ)
Nginx + PHP-FPM + PHP 5.6 + CakePHP 2.8
↓(第4フェーズ)
Nginx + PHP-FPM + PHP 5.6 + CakePHP 2.10
↓(第5フェーズ)
Nginx + PHP-FPM + PHP 7.x + CakePHP 2.10

今回、第4フェーズを終えることができました。

続きまして、第5フェーズである、PHP7化に着手いたします。

PHP5.3→5.6移行が完了しました

kanazawa|2019年04月17日
Ansible

SREチームの金澤です。

PHP5.3→5.6バージョンアップが完了しましたので報告いたします。

CakePHP1.3→2.8バージョンアップが完了してから約2カ月での移行となりました。

2019/03/20にコネヒトさんで開催されたPHP勉強会で、その詳細について発表させていただきました。
(このときは管理画面、バッチサーバーまで移行完了していました)

PHPのバージョンアップ自体は3月中に完了していましたが、その後関連ライブラリのバージョンアップも併せて行いました。

バージョンアップに向けたCI改善

2019/2/5にCakePHP1.3→2.8バージョンアップが完了し、その後すぐにCI周りの改善に取り掛かりました。

その詳細をPHP5.6化に向けたCircleCIのアップデートにまとめています。

目的は以下の2点です。

  • PHP5.6の文法に対応する
    • @PSR2を設定
    • @PHP56Migrationを設定
  • CIでUTを回す
    • UTが通らなければマージできないようにする

※PHP5.6との互換性を検証したく@PHP56Migrationを設定しましたが、これはあまり活躍しませんでした。
(PHP-CS-Fixerの@PHP56MigrationはPow関数の対応しかサポートされていないようです)
https://mlocati.github.io/php-cs-fixer-configurator/?version=2.2

PHP5.6バージョンアップ準備

PEARライブラリのコード管理

ランサーズは一部の機能にPEARライブラリを利用しています。
新規にサーバーをAnsibleで構築するにあたり、pear installでインストールしていました。

しかし、pear installでインストールすると/usr/share/srcに配置されるため、ライブラリ管理がOS側に依存していました。
2019/1にpear.php.netが数週間停止する事態が起きたこともあり、php.pear.netに依存しないサーバー構築手順に変更しました。

利用していたPEARライブラリは以下2つです。

  • Math Stats
    • Vendorディレクトリに直接配置
  • Crypt Browfish
    • Composerで管理

Crypt Browfishは依存関係があり、単純にVendorディレクトリに配置するだけでは動作しないため、
composer.jsonに以下のように記述して管理するようにしました。

  "repositories": [
    {
      "type": "vcs",
      "url": "git@github.com:pear/Crypt_Blowfish.git"
    }
  ],
  "require": {
    "pear/crypt_blowfish": "dev-master#8a56b74",
  }

※これらのライブラリはPHP7に対応していないので、代替手段を用意しておきたいところです。

AWS SDK PHPをV2に統一

AWS SDKについては、V2をメインに利用していましたが、S3関連の処理だけV1を利用していました。
AWS SDK V1はPHP5.6の環境ではStrict Warningを出してしまうため、すべてV2に統一しました。

PHP5.6コンテナの構築と切り替え

まず開発用コンテナのPHP5.6化から始めました。

ランサーズの開発環境はDockerを利用しており、docker-compose.ymlには以下のように記述しております。

version: '2'

services:
  app:
    image: xxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/lancers_app:latest
    hostname: app
    networks:
      lancers:
        ipv4_address: 10.100.6.11
    extra_hosts:
       - "dev.lancers.jp:10.100.50.11"
…
    container_name: app-6-11
    volumes:
      - ~/www:/var/www

PHP5.3環境とPHP5.6環境をすぐに切り替えられるようにしました。
構築したPHP5.6のコンテナはタグ名を5.6とし、docker-compose.override.ymlを以下のように記述します。

version: '2'

services:
  app:
    image: xxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/lancers_app:5.6

PHP5.6で稼働させたい場合は、このdocker-compose.override.ymlを配置して

$ docker-compose up -d

とすれば、PHP5.6コンテナにすぐに切り替わります。

PHP5.3コンテナに戻したい場合は、imageの行を#でコメントアウトし、再度

$ docker-compose up -d

とすれば戻ります。

PHP5.6対応

以前PHP5.6コンテナに切り替えて検証したときには、
CakePHP1.3とAWS PHP SDK V1のソースが大量のStrict Warningを出していました。

今回、CakePHP2.8とAWS PHP SDK V2にそれぞれバージョンアップして臨んだのですが、
まだStrict Warningを出している箇所が残っていました。

親クラスと子クラスの引数を合わせる

一番多かったのは、親クラスと子クラスの引数が一致していないパターンです。
PHP5.3では警告されませんでしたが、PHP5.6では以下の警告が出力されます。

Strict Standards: Declaration of WorkTask::afterSave() 
should be compatible with Model::afterSave($options = Array) 

例えば、この例では、WorkTask::afterSave関数の引数が以下のように定義されているのに対し、

public function afterSave($created)

親クラスのModel.phpでは、以下のように定義されていました。

public function afterSave($created, $options = array())

CakePHP1.3→2.8にバージョンアップした過程で引数の仕様変更があったためです。

以下のソースの子クラス全関数の引数をチェックし、親クラスと引数を一致させるように修正しました。

  • Model.php
    • Behavior.php
  • Controller.php
    • Component.php

参照を引数に取る関数の対応

PHP5.4からの仕様変更。
例えば、以下のコードで

$key = array_shift(array_keys($data));

以下の警告が出力されます

Strict (2048): Only variables should be passed by reference

参照を引数にとる関数は一度変数に格納する必要がありました。

$keys = array_keys($data);
$key = array_shift($keys);

参照が戻り値の関数の対応

PHP5.4からの仕様変更。
PHP5.3で戻り値を参照として受け取っていると

$Db =& ConnectionManager::getDataSource($model->useDbConfig);

以下の警告が出力されます

Strict (2048): Only variables should be assigned by reference

これは実体のまま受け取るように修正して対応しました。

$Db = ConnectionManager::getDataSource($model->useDbConfig);

初期化せずにオブジェクトとみなして代入

PHP5.6では以下のようにいきなり代入すると、

$record->type = 'type1’;

以下の警告が出力されます

Warning Error: Creating default object from empty value in ...

オブジェクトのメンバ変数を初期化する場合は、まずオブジェクト自体の初期化が必要です。
※入れ子の場合でも初期化が必要

$record = new stdclass();
$record->type = 'type1';

$record->fields = new stdclass();
$record->fields->key = 'key1';

PHP5.6化後のパフォーマンス

サーバーレスポンス

移行前に160ms前後であったものが140ms前後に改善。
アクセラレータがAPC → APCu + OPCacheになりましたが、その効果が出ていると推測しています。

バッチの実行時間

最も時間がかかっていた7時間のバッチが、30分ほど短縮されました。
全体的には、約1.13倍程度のパフォーマンスアップになりました。

関連ライブラリのバージョンアップ

PHP5.6化に伴い、composerで管理している各種ライブラリのバージョンアップも行いました。
詳細は別途ブログに書きたいと思います。

バージョンアップ資料の共有

PHP5.6バージョンアップ手順の詳細をGithubで公開しました。
https://github.com/LancersDevTeam/PHP_versionup

今更感はありますが、今後、移行する方の参考になりましたら幸いです。

今後の予定

ランサーズのPHP、CakePHPバージョンアップは、以下のフェーズに分けて取り組んでいます。

Apache + mod_php + PHP 5.3 + CakePHP 1.3
↓(第1フェーズ)
Nginx + PHP-FPM + PHP 5.3 + CakePHP 1.3
↓(第2フェーズ)
Nginx + PHP-FPM + PHP 5.3 + CakePHP 2.8
↓(第3フェーズ)
Nginx + PHP-FPM + PHP 5.6 + CakePHP 2.8
↓(第4フェーズ)
Nginx + PHP-FPM + PHP 5.6 + CakePHP 2.10
↓(第5フェーズ)
Nginx + PHP-FPM + PHP 7.x + CakePHP 2.10

今回、第3フェーズを終えることができました。

続きまして、第4フェーズである、CakePHP2.10化に着手いたします。
これが終われば、いよいよPHP7化に着手できます。

PHP5.6もすでにセキュリティサポートが切れていますのでできるだけ早くPHP7化に着手したいところです。