Technote

by sizuhiko

GAE gen1 で動いている PHP5.5 で作った個人開発サービスを gen2 PHP8.2 へ移行した1年記 〜 その 1

2023年2月3日にGoogle Cloudからメールが届きました。

ここで作っていた個人開発のサービスとは Toilet Evolution です。一時期に流行ったトイレ利用状況可視化のIoTサービスです。

PHP + Web Components の SPA 構成でできていて、Google App Engine(以下GAE)の第一世代で動いています。 当時ほぼ無料で動かせるPaaSというのはあまりなくて、GAEばかりを選択していたと記憶しています。 その後 Heroku とか出ましたけど、実質それなりの利用で月100円もかからずに運用しつづけられている状況なのでありがたいです。

GAEのランタイム サポート スケジュール によると PHP5.5 は 2024年1月30日にサポートの終了で、非推奨が2026年1月31日ということ。廃止は書かれていません。

と同時にまだ使っている人もいるので、簡単にメンテ大変なんでクローズしますというのも違いますね。ユーザー大事。ありがたや。

そこで、サポート終了状態でずっと動かすのもなと思い、最新版のPHPへの移行を検討し始めました。

一番最後のメンテナンス

このWebサービスは長いことやっているのですが、直近で記事にしたのは Toilet EvolutionのフロントエンドをPolymer3対応する(4) で、2019年4月。あのあとコロナがあっての5年ぶりのメンテになります。

移行前の composer.json

以下のような依存バージョンでした。

{
    "php": ">=5.5.0",
    "slim/slim": "^3.1",
    "monolog/monolog": "^1.17",
    "tomwalder/php-gds": "^2.1",
    "tuupola/slim-basic-auth": "^2.0",
    "nesbot/carbon": "^1.21",
    "league/oauth2-client": "^1.4",
    "league/oauth2-google": "^1.0",
    "akrabat/rka-slim-session-middleware": "2.0.0-RC1",
    "sizuhiko/slim3-csrf-utilities": "dev-master",
    "justinrainbow/json-schema": "^2.0",
    "phpunit/phpunit": "^4.8",
    "helmich/phpunit-psr7-assert": "^1.1",
    "gecko-packages/gecko-memcache-mock": "^2.1",
    "google/appengine-php-sdk": "^1.9"    
}

歴史を感じますね。

GAE gen1 から gen2 で何が変わるのか

GAEでの移行ガイドが出ていて、PHPだと PHP 5.5 と PHP 7 / 8 の違い というドキュメントがあります。

PHP公式ドキュメントの移行ガイドへのリンクと、GAE特有のアップグレードガイドが書かれています。 タイトルだけ拾うと、以下のような移行が必要になるということでした。

  • app.yaml ファイルを移行する
  • ランタイム制限の緩和
  • App Engine の PHP SDK から移行する(レガシーバンドルサービスAPI利用方法)
  • ローカルでの実行方法

レガシーバンドルサービスとは PHP 7 / 8 用の以前のバンドル サービスにアクセスする で書かれているサービスで、今回のサービスで言うと Memcache とセッションを利用していました。

で、これらは使えるので、使い続けて良いのか?という選択を迫られます。 もう少し関連するドキュメントを読んでみましょう。 以前のバンドル サービスの概要 によると

レガシー ランタイムの一部はオープンソース コミュニティで管理されなくなったため、App Engine デベロッパーは、新しいランタイムに移行すべきかどうか難しい判断に迫られる可能性があります。こうした移行には時間と労力がかかりますが、レガシー ランタイムの使用を継続すれば、アプリの維持コストが増大する結果になります。 こうした課題を踏まえ、Google Cloud では新しいランタイムへの移行パスを段階的に提供していく予定です。Google Cloud では、ランタイム移行の複雑さを軽減するために、Python 3、Java 11、Go 1.12 以降、PHP 7 / 8 などの第 2 世代ランタイムで App Engine の従来のバンドル サービスとその関連 API をサポートしています。

移行大変なんで段階的な移行パスを用意するよってことですね。 Memcache はこの移行バスに入ったようでそのままでも利用できたんですが、あとでまた移行するのが面倒そうなんで、このタイミングでやっておきたいと考えました。

レガシーランタイムからの移行に関してのドキュメントは レガシー バンドル サービスからの移行 にあって、参考になります。

実行目標

  • PHPのバージョンを最新化する
  • GAEのレガシーランタイムからの脱却

というのを掲げてみました。

けど、PHP自体あんまり触ってない(たぶん7を少しやったぐらいで、実質5系の知識で止まっている)ため、長期戦は予想していました。当時はまだ1年ぐらいあるから何とかなるやろ、ぐらいに思っていた自分がいました。

そして、その後の1年ぐらいで移行を完了させて、現在は GAE gen2 の PHP8.2 で稼働しています。👏👏

このあと数回にわたって、どうやって移行してきたかをコミットログと共にふりかえっていきたいと思います。 少しでも PHP のバージョンアップや GAE gen2 へ移行する人の助けになればな、と思っています。

PHPカンファレンス関西2024に参加して、オフラインカンファレンスの盛り上がりを体験してきた

大阪自体はそれほど久しぶりではないけど、東京以外のカンファレンスにリアル参加するのは久しぶりだった。 ブログを振り返ると、PHPカンファレンス沖縄2019で、 標準インターフェースを使ったアプリケーション開発について発表してきましたがコロナ前最後に参加した地域PHP conであったようだ。 大阪へは藤井風のパナスタライブ以来なので、1年半ぶりぐらい。

このところのホテル事情

昨年秋のシルバーウィークに九州まで車でスカイラインを巡る旅をしてきて、ホテルの混雑状況とか把握していたつもりでしたが、今回はやばかった。 ちょうど3連休開催ということで、ホテルの値段がやばい。大阪ではいつも宿泊する本町のホテルが早々に売り切れててどうしよう?となりましたが、 運よくリニューアルしたばかりの東横inn淀屋橋駅南が空いてて、普段の東横価格だったので安心できました。

前夜祭への参加

さらに前日入りをすることにしていたのですが、午前中に東京で用事があったので 【非公式!前夜祭】 PHPカンファレンス関西2024 へも新幹線に乗るタイミングで間に合いそうだったので参加表明するという慌ただしさ。

前夜祭で他の人の発表を聞いていて、自分も何か話したいなーという欲が出てきたので、その場でスライドをさくさくと作ってWebアクセシビリティの話をしました。 いろいろなところで4月から義務化?!とか言われてますが、実際のところどうなのよ?みたいなところを。 最新関係する仕事もしていてかなり調べたので、気になっている人もいるのでは?と思ったのです。

かつてはカンファレンスを開催していた大阪産業創造館という場所も懐かしかった。

前夜祭を楽しんだ後は、最近大阪に来たら必ずいく Bar で飲んでちゃんと終電ではホテルに戻りました。

カンファレンス当日

今回はあまり普段登壇で見かけない人の発表を聞く、久しぶりに会うコミュニティメンバーと会話する、という2つの目的で参加していました。 どちらも当初の見込みは達成できて、たくさんの刺激をもらいました。 午後は主にアンカンファレンスの会場にいて、枠も空いていたので前夜祭で話した内容をまたやってみたりと、聞くだけでなく積極的にイベントにも参加しました。 ちゃんとスポンサークイズにも応募したよ!(半分ぐらいは聴けてなかったので勘で回答しちゃいましたがw)

懇親会 -> 2次会 -> ホテルそばで1人3次会 をやって終了。 翌日の月曜日も3連休ということで休みでしたが、東京で推しのライブがあるのでお昼には新幹線に乗り込みました。 いつものネギ焼きは新幹線のフードコードで

あ、その前にもちろん聖地巡礼(阪急梅田のタイガースショップ)もして、グッズも買いました。

久しぶりの地域カンファレンスに参加してのまとめ

昨年のPHPカンファレンス福岡に始まり、地域カンファレンスの開催も戻ってきて、今回の大阪に久しぶりに参加しました。 大阪のPHPcon自体は PHPカンファレンス関西2018に参加してGAEに継続的デプロイする方法について発表してきました のとき以来で、今回はこのときの登壇者Tシャツを来て参加しました。

つまり関西では 6年ぶりということですね。 オフライン開催のPHP系カンファレンスに関しても昨年の福岡などより前となると、おそらく PHPerKaigi 2020で「E2Eテストに向き合う」ついて発表してきました このときの PHPerKaigi がコロナ前最後であったという記憶があります。

それでも4年。 つまり6年以上前からコミュニティに参加していた関西圏の人以外はほぼ初参加となるわけです。

今年は月刊PHPカンファレンスなんて言葉が出るぐらいですが、それも昨年の福岡に始まった地域カンファレンスの楽しさから触発されたもので、しばらく行われてこなかった反動だったり、この4から6年の間に新たにこの業界で働き始めた人たちの新しい交流の場なんだなというのを実感しました。

今回のPHPCon関西ではいくつもの素晴らしい取り組みがありました。

  • 公式 note で発信
  • PHPerシール
  • お誘いチケット
  • おすすめトーク診断

まず公式 note は X だけで発信していると流れてしまいそうな情報に簡単にアクセスできるのもあり、良いなーと思いました。 困ったら note 見る、みたいな動線がある。もちろん公式HPに載せるのもありなんで、そこは運用の手間がどちらが少ないかという判断もあるでしょう。

続いて PHPerシール。こちらは2020のPHPerKaigiで行われていたトレーディングカードにインスパイアされたものだと思いますが、積極的に交流できるようにするアイテムとしてとても良いと思いました。スピーカーにはレアカードがあったりしたのも良かったと思います。

お誘いチケットは有料イベントで参加を迷っている人を誘いやすい仕組みとして良いなと思いました。 PHPerシールとか当日の交流の仕組みがあっても、まず参加してくれないと届きませんね。

おすすめトーク診断も、誘ってもらったり紹介してもらったので参加するけど、カンファレンスの歩き方ってわからんな?という人も向けて楽しみながら参加できる仕掛けだったと思います。

つまりPHP関西自体に初参加、ましてやコロナで勉強会を含めオフラインのイベントそのものに初参加ということが6年というブランクから容易に想像できる環境での地域コミュニティの盛り上げ方に優れた戦略が練られていたことがわかります。

  1. お誘いチケットで誘って参加してもらい
  2. おすすめトーク診断で当日迷わないようにして
  3. PHPer シールで当日は交流してもらう
  4. なんなら X のアカウント持ってなかったけど、シール経由で作ってもらって今後の交流やPHP関西からの発信を受け取ってもらえるようにする

委員長の閉会の挨拶でもこれらを話されていましたが、関西でのPHPコミュニティを盛り上げたいという熱い想いを聞いて、また来年開催されたら参加したいな、発表もしてみたいなと思いました。

久しぶりということで運営の経験値とかもリセットされた中での開催だったと思うけど、そういうのを感じさせない良いカンファレンスでした。 参加のみなさま、久しぶりに再会したみなさま。そして何より運営のみなさまありがとうございました。

マルチプルレポをモノレポへコミットログを残しながら移行する

背景

プロジェクトで複数のAPIサーバーや、マイクロサービスなどを展開するのに、OpenAPI の定義を置くリポジトリを個々に作っていたのですが、メンテナンス性を考えてマルチプルレポからモノレポへ移行することにしました。

基本的に OpenAPI の定義(yamlファイル)の内容と、デプロイ先(AWS S3のWebホスティング先バケット)が違うぐらいで、それ以外の内容はまったく一緒であるリポジトリが複数ある感じです。

  • app-a-apidoc
  • app-b-apidoc
  • service-account-apidoc
  • service-payment-apidoc

みたいなマルチプルレポを apidoc モノレポへまとめていきます。

各 apidoc は swagger-ui-dist に yaml ファイルを入れて、Webホスティングしている S3 バケットにデプロイしています。 ビルドスクリプトは gulp で、デプロイは serverless framework に serverless-s3-sync プラグインを入れて実行しています。

リポジトリの移行の準備

リポジトリを移行するにあたり Keeping git history when converting multiple repos into a monorepo という記事がとても役にたったので、こちらの手順を参考にして紹介していきます。

ディレクトリ構造決定

まず最初にモノレポ移行後のディレクトリ構造を検討します。 よくある npm のパッケージをモノレポにしている場合だと

packages
  +-- package-a
  +-- package-b

みたいな階層にすることが多いんじゃないかな?と思います。 参考記事だと projects/* のような感じですね。 aws-sdk v3 だと clients や lib, packages など目的別にいろいろ分けているようです。

そこで今回私たちは

apps
  +-- a
  +-- b
services
  +-- account
  +-- payment

みたいなディレクトリ構造にしました。これはそれぞれのプロジェクトや、まとめたいリポジトリにもよるので、個別に検討しましょう。

既存リポジトリのディレクトリ変更

ディレクトリ構造が決まったら、既存リポジトリをそのディレクトリに合わせて階層を変更します。

cd app-a-apidoc
mkdir -p apps/a
git mv -k * apps/a
git mv -k .* apps/a
git commit -m "chore: move all files into apps/a"
  1. 既存リポジトリをチェックアウトしたディレクトリに移動
  2. モノレポにしたときのディレクトリツリーを作成
  3. 2のディレクトリ下にすべてのファイルを移動。移動には git mv コマンドを使う
  4. コミット

この作業を移行対象のすべての既存リポジトリで実施していきます。

モノレポの作成

GitHub上でリポジトリを作ってチェックアウトするか、 git init コマンドでローカルにモノレポのフォルダを準備します。 続いて、モノレポリポジトリにワークスペースのルートディレクトリを作っておきます。

mkdir apps
mkdir services

git remote add を使ってローカルディレクトリのリポジトリ(既存リポジトリのディレクトリ変更でディレクトリ変更してコミットしたもの)を追加します。

git remote add -f app-a-apidoc ../app-a-apidoc
git remote add -f app-b-apidoc ../app-b-apidoc
git remote add -f service-account-apidoc ../service-account-apidoc
git remote add -f service-payment-apidoc ../service-payment-apidoc

で、それらをモノレポの中にマージしていきます。マージするブランチが main であると仮定します。

git merge app-a-apidoc/main --allow-unrelated-histories
git merge app-b-apidoc/main --allow-unrelated-histories
git merge service-account-apidoc/main --allow-unrelated-histories
git merge service-payment-apidoc/main --allow-unrelated-histories

最後にリモートを削除しましょう。

git remote remove app-a-apidoc
git remote remove app-b-apidoc
git remote remove service-account-apidoc
git remote remove service-payment-apidoc

モノレポの設定

ここまでで既存のリポジトリを1つのモノレポにコミットログ付きで合体できました。 続いて、モノレポで開発作業ができるように環境整備をしましょう。

ワークスペースを設定する

モノレポのルートディレクトリで npm init コマンドを実行して package.json を作ります。 作成した package.json にワークスペースに関する設定を追加します。

  "private": true,
  "workspaces": [
    "apps/*",
    "services/*"
  ],

依存関係の設定

続いて、各リポジトリで使っていた devDependenciesdependencies の依存関係をモノレポのルートディレクトリにある package.json に移動します。 通常は各リポジトリ共通のものだけですが、今回の apidoc ではすべて共通だったので、そのままコピペしました。

ワークスペース(apps や services)にある package.jsondevDependenciesdependencies を削除し、 package-lock.json も削除します。

最後にルートディレクトリで npm i を実行して依存関係をインストールします。

各 apidoc の整理

.npmrc.gitignore , .editorconfig など必要な設定ファイルをワークスペースからルートディレクトリに移動しておきます。

その他共通になったものはルートディレクトリ下に移動しておくと良いでしょう。

GitHub Actions の統合

ワークスペース(apps や services)にある .github をどれかルートディレクトリに移動して、ワークスペース側からは削除します。 単に全部のワークスペースをビルドしたりデプロイするだけなら -ws オプションを追加するとすべてのワークスペースに対してコマンドを実行するようになります。

    steps:
      - uses: actions/checkout@v3
      - run: npm ci
      - run: npm run -ws build
      - run: npm run -ws deploy

まぁでも通常はそんなことなく、以下のパターンを考慮する必要があります。

  • ルートディレクトリの package.json で定義している依存関係にアップデートがあった(dependabotなど)
  • ワークスペースのファイルが修正された

前者はすべてのワークスペースに対して Action を実行したほうが良いのですが、後者は変更があったワークスペースだけの実行に絞り込みたいところです。

そこで利用できるのが Get changed workspaces action です。

jobs:
  get-changed-workspaces:
    outputs:
      packages: ${{ steps.changed-packages.outputs.packages }}
      empty: ${{ steps.changed-packages.outputs.empty }}
    steps:
      - name: Check out repository code
        uses: actions/checkout@v3
      - name: Find changed workspaces
        uses: AlexShukel/get-changed-workspaces-action@v2.0.0
        id: changed-packages
  deploy:
    needs: [get-changed-workspaces]
    if: ${{ !fromJson(needs.get-changed-workspaces.outputs.empty) }}
    strategy:
      matrix:
        package: ${{ fromJson(needs.get-changed-workspaces.outputs.packages) }}  
    steps:
      - uses: actions/checkout@v3
      - run: npm ci
      - run: npm run -w ${{ matrix.package.name }} build
      - run: npm run -w ${{ matrix.package.name }} deploy

この Action を使うと変更があったワークスペースの名前を配列型式で取得できます。 ワークスペースに変更がないときは empty でわかるので、 if 文で制御して何もしないように設定も可能です。

で、特定のワークスペースに対して npm コマンドを実行したい場合は -w ワークスペース名 オプションを指定します。

そこで、 .github/workflows には以下のファイルを設置することにしました。

  • build-all.yml
  • build.yml

前者は ルートディレクトリの package.json で定義している依存関係にアップデートがあった(dependabotなど) を想定していて、以下のような条件で起動したら、 -ws オプションですべてのワークスペースで実行されるようにしています。

on:
  workflow_dispatch:
  push:
    paths:
      - package.json
      - package-lock.json
    branches:
      - master
jobs:
  deploy:
    steps:
      - uses: actions/checkout@v3
      - run: npm ci
      - run: npm run -ws build
      - run: npm run -ws deploy

後者は Get changed workspaces action を使って個別のワークスペースビルドされるように設定しました。

さいごに

マルチプルレポ、モノレポ、それぞれに良いところはあると思います。 必ずモノレポが良いというものでもないでしょう。 それぞれのプロジェクトやリポジトリの運用を見極めて選択していきたいですね。

マルチプルレポからモノレポへ移行するときに、本記事が参考になれば幸いです。

tsyringe を TypeScript 5 で使う方法

tsyringe は、Microsoft のオーガナイゼーションにある、JavaScript/TypeScript用のDIコンテナです。

Microsoft は TypeScript の親だし、DIコンテナあるんだったら使うよなーというぐらいの理由で使っていましたが、ここ数年はアップデートのリリースがありません。 この記事を書いている時点での最新版は、2020/11/9 に出た v4.4.0 です。

TypeScript 5 でコンパイルエラーになる

tsyringe を TypeScript 5 で利用しようとすると、コンパイルエラーになります。 で、さすがに利用者が多いライブラリだけあって、すぐ issue TypeScript 5.0 Support of tighter parameter decorator checking が出ました。 しばらくしたら PR fix: allow propertyKey to be undefined も出ました。PRが 2023/3/26 に出て、マージされたのは 2023/4/17 です。

それから2週間ぐらい経過しましたが、リリースされる気配はありません….

でも TypeScript 5 を使いたいんや

まぁそう思いますよね。 僕らのプロジェクトでもそう思ったんで、パッチを作りました。

プロジェクトルート(package.json とか置いているディレクトリ)に tsyringe.d.ts を置いて、型定義をオーバーライドする感じです。 例えば以下のような感じです。

import tsyringe = require('tsyringe/dist/typings/index');

declare namespace t {
  function inject(
    token: tsyringe.InjectionToken<any>
  ): (target: any, propertyKey: string | symbol | undefined, parameterIndex: number) => any;
  const injectable: typeof tsyringe.injectable;
  const container: typeof tsyringe.container;
  type DependencyContainer = typeof tsyringe.container;
}

export = t;

まずコンパイルエラーになるのは inject 関数なので、それをオーバーライドします。 次に、プロジェクトで import している関数や型があれば、それぞれ consttype で宣言して require した内容からそのまま export するようにしました。

これはマージされている PR が型定義ぐらいしか修正しておらず、実装に関しては何も修正されていなかったので、問題なしと判断して型定義だけをオーバーライドしたパッチを作ることにしました。

で、どうなの?

現時点動作に問題はなく、普通に TypeScript 5 で tsyringe が使えています。 PRがマージされて、すぐリリースされるんじゃないか?と思ったので、この内容を記事にする必要もないかな?と思っていたのですが、 リリースされる気配を感じないので、僕らのやっている方法が少しでも役に立てばと思い、記事にしました。 はやく TypeScript 5 対応が正式リリースされて欲しいですね。

LocalStack を使って aws-sdk の Integration Test を実行する

AWS Lambda の Node.js 14 を 18 に移行する(aws-sdk v3 移行編) の中でも書いたように、aws-sdk v3 を使ったコードを UnitTest するには AWS SDK v3 Client mock を利用してモックした方が簡単です。

一方で少し IntegrationTest をしたいと思うこともあるでしょう。そうしたときは LocalStack の利用が便利です。 手元も環境なら Docker の Extension で動きます。 CI 環境、たとえば GitHub Actions ならサービスでコンテナ起動すれば大丈夫です。

インテグレーションテストでのエンドポイント

こんな感じでテストコードで endpoint を切り替えられるようにしておくとローカルとCI環境のどちらでもテストを実行しやすくなります。

const options = {
  credentials: { accessKeyId: 'dummy', secretAccessKey: 'summy' },
  endpoint: `http://${process.env.TEST_AWS_HOSTNAME ?? 'localhost'}:4566`,
  region: 'ap-northeast-1',
};
const stepFunctions = new SFNClient(options);

手元では localhost で良いし、CI環境(たとえば GitHub Actions ) なら、以下のようにしておくと良いです。

    container: node:18
    services:
      localstack:
        image: localstack/localstack
    env:
      TEST_AWS_HOSTNAME: localstack
    steps:
      - run: npm t

LocalStack v2.0 以降での変更点

最近バージョンが 2.0 になった LocalStack ですが Lambda 関連で大きな変更がありました。 基本的に Lambda を利用しなければ問題はありませんが、特に問題なければ localstack サービスを以下のように設定しておくと安心です。

    container: node:18
    services:
      localstack:
        image: localstack/localstack
        env:
          LAMBDA_SYNCHRONOUS_CREATE: 1
        volumes:
          - /var/run/docker.sock:/var/run/docker.sock

Actions のホストコントローラの docker.sock をマウントすることで LocalStack の中で Lambda が起動できるようになります。

詳しくは公式の v2.0.0 リリースノートLambda 部分に書いてありますので、読んでみてください。

さいごに

単体テストで LocalStack 使うのはやりすぎだと思いますが、部分的にインテグレーションテストしたい、といった場合には有用な方法かな、と思いますので、用途に応じて使い分けられると良いですね。