見出し画像

#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イメージを動かすことができています。


この記事が気に入ったらサポートをしてみませんか?