#21_Github ActionsでCloud Spanner対応のPHPイメージをビルドするための工夫
こんにちは、WFSでサーバエンジニアをしている藤田です。
以前の記事で、WFSではPHP + Cloud Spannerを使ってゲーム開発をしていることを紹介しました。
今回は、開発環境として利用しているDockerのイメージビルドについての工夫を紹介したいと思います。
Dockerの利用
私たちはサービスにKubernetesを使っているため、サーバの開発環境にDockerを利用しています。まず、ここでの小さな工夫について紹介します。
それは、プロジェクトのDockerfileの親イメージを、PHPのオフィシャルイメージではなく、自分たちでビルドしたイメージにすることです。
これは、たんに便利だというのもありますが、PHPのgRPC拡張のビルド時間が長いことが主な理由です。
gRPC拡張は、PHPからCloud Spannerに接続するために必要ですが、M1 MacBook Proではビルドに10分以上かかります。
Dockerにはビルドキャッシュがありますが、キャッシュをクリアしたりすると再ビルドが必要になり、それがエンジニアの待ち時間となってしまいます。また、プロジェクト全体の開発サーバはgitのメインブランチの最新が常にデプロイされるため、ビルド時間が長くなると開発環境の反映リードタイムにも影響が出ます。
そこで、gRPC拡張を含んだイメージを事前にビルドしておくことで、ビルド時間が長くかかる部分をイメージのpullで済むようにしました。これによりビルド時間の問題を大きく改善することができました。
新しい問題
この運用はうまく動いていましたが、M1というAppleが開発した新しいCPUとそれを搭載した新しいMacの登場により難しい問題が発生しました。
Apple Siliconと呼ばれる、もともとiPhoneやiPodのCPUとして開発されたCPUは、ARMという、パソコンで多く使われてきたIntelのCPUとは異なるCPUアーキテクチャが使用されています。そのARMアーキテクチャのCPUが、M1によってパソコンに搭載されるようになった訳です。
現在我々がお客様へのサービスを提供するために利用しているサーバのCPUはIntelアーキテクチャです。一方、エンジニアは人によってWindowsやMacで開発をしていましたが、いずれにせよそれらのCPUもIntelアーキテクチャでしたから、1つのDockerイメージを開発から運用まで使い続けることに問題はありませんでした。
しかし、M1登場以降、エンジニアの開発マシンもだんだんとApple Siliconのマシンに置き換わっていき、IntelアーキテクチャのイメージをARMアーキテクチャのCPUで動かすことが増えてきました。
ここで問題になったのがパフォーマンスです。Rosetta 2がありますので、IntelアーキテクチャのDockerイメージをApple Siliconのマシンで動かすことは可能です。しかし、PHPのパフォーマンス、特に静的解析の速度が明らかに遅くなりました。
以下は、実際にM1 Macbook Proで測定してみた結果です。
縦軸はDockerイメージのアーキテクチャです。数字は、それぞれのツールを現在サービスしているあるゲームのサーバコードで動かした秒数です。それぞれ10回ずつの平均値です。
phpcbfで10倍程度、psalm、phpstanはそれぞれ5倍程度実行時間が増えていることがわかります。
(本来であればIntel MacでIntelアーキテクチャのイメージを動かした時の結果もあるべきなのですが、同等環境を準備することができませんでした。申し訳ありません)
以上の結果から、ARMアーキテクチャのイメージを用意する必要が出たのですが、先に書いたように、サービスにはIntelアーキテクチャのイメージが必要です。
そこで、ベースイメージはマルチアーキテクチャでビルドすることにしました。
マルチアーキテクチャビルド
IntelアーキテクチャのみのベースイメージのビルドはもともとGithub Actionsで行っていたため、マルチアーキテクチャビルドもGithub Actionsでビルドできるようすすめました。
最初に作ったワークフローは以下のようになります。
name: Build and Push
on:
push:
branches:
- 'main'
- 'releases/**'
defaults:
run:
shell: 'bash -Eeuo pipefail -x {0}'
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build:
permissions:
contents: 'read'
id-token: 'write'
strategy:
matrix:
os: [ubuntu-latest]
php:
- 8.0.28
- 8.1.18
- 8.2.5
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
-
name: Set up QEMU
uses: docker/setup-qemu-action@v1
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v4
with:
images: image-name
tags: |
type=raw,value=${{ matrix.php }}
- id: auth
uses: google-github-actions/auth@v1
with:
token_format: 'access_token'
workload_identity_provider: 'provider'
service_account: account
- name: Login to GCR
uses: docker/login-action@v2
with:
registry: registory
username: oauth2accesstoken
password: ${{ steps.auth.outputs.access_token }}
- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
file: Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: PHP_VERSION=${{ matrix.php }}
もともとIntelアーキテクチャのイメージをビルドしていたワークフローからの変化は、docker/build-push-actionのplatformsにlinux/arm64を追加しただけです。
イメージはGoogle CloudのArtifact Registryで管理するためにOIDCによる認証をしていますが、Dockerイメージをビルドするワークフローとしてはオーソドックスな形だと思います。
しかし、これは失敗しました。
なにがおこったかというと、OIDCのトークンの有効期限が切れてpushに失敗しました。
もともと数十分だったビルド時間が4時間にのびたためです。
時間の大半は、ARMアーキテクチャのgPRC拡張をビルドしている時間です。
解決方法は、シンプルに以下のようにしました。
steps:
- uses: actions/checkout@v3
-
name: Set up QEMU
uses: docker/setup-qemu-action@v1
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v4
with:
images: image-name
tags: |
type=raw,value=${{ matrix.php }}
- name: Pre Build
uses: docker/build-push-action@v4
with:
context: .
file: Dockerfile
platforms: linux/amd64,linux/arm64
push: false
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: PHP_VERSION=${{ matrix.php }}
- id: auth
uses: google-github-actions/auth@v1
with:
token_format: 'access_token'
workload_identity_provider: 'provider'
service_account: account
- name: Login to GCR
uses: docker/login-action@v2
with:
registry: registory
username: oauth2accesstoken
password: ${{ steps.auth.outputs.access_token }}
- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
file: Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: PHP_VERSION=${{ matrix.php }}
OIDC認証の前にビルドをしておき、認証後はビルドキャッシュを使うだけの状態にしました。
これにより、ビルド時間が長いイメージでもOIDC認証を使ってpushできるようになりました。
まとめ
私たちは、PHP + Cloud Spannerでサービスを提供しています。開発に使うDockerイメージは、Cloud Spannerに必須のPHPのgRPC拡張のビルド時間が長いため、あらかじめビルド済みのDockerイメージを用意していました。
しかし、Apple Siliconの登場によりマルチアーキテクチャのDockerイメージを準備する必要が出てきました。
マルチアーキテクチャのDockerイメージはGithub Actionsでビルドすると4時間かかるため、ワークフローを少し工夫してビルドできるようになりました。
現在は、エンジニアのMacでもサービスでもCPUアーキテクチャに即したDockerイメージを動かすことができています。