🙌

【GitLab CI】LaravelのCI/CD実行時間短縮

2024/12/16に公開1

概要

業務でLaravelアプリケーションのCI/CD実行時間短縮を行なったため、調査内容などや対応についてまとめる。

構成

Laravelアプリケーションがユーザーからのリクエストを受け付けDBへレコードを追加し、後続のMicroserviceが取得し処理を行うような構成(詳細は割愛)。

DBはlaravelとは別リポジトリでマイグレーション管理されている。git submoduleを使用して変更を取り込み、docker composeでpostgresコンテナへマイグレーションを行い開発を行う。

レジストリにはDockerHubを使用。

compose.yml
services:
  php:
  (略)
  postgres:
    image: postgres
  (略)
  migration:
    depends_on:
      - postgres

従来のパイプライン

従来のパイプラインは以下の順に実行されていた。

.setup_db:
  stage: setup
  variables:
    POSTGRES_PORT: 5432
    POSTGRES_USER: "root"
    POSTGRES_PASSWORD: "password"
    DBNAME: "dbname"
  before_script:
    - |
      docker run \
        -e POSTGRES_ADDRESS="${POSTGRES_ADDRESS}" \
        -e POSTGRES_PORT \
        -e POSTGRES_USERNAME="${POSTGRES_USER}" \
        -e POSTGRES_PASSWORD \
        -e POSTGRES_DBNAME="${DBNAME}" \
        example/migration:latest

test:
  stage: test
  services:
    - docker:24.0.5-dind
    - postgres:15.2-alpine3.17
  extends: .setup_db
  script:
    - docker build  --target development -t test -f docker/php/Dockerfile .
    - |
      docker run -t -v $(pwd):/workspace test sh -ec " \
      composer install && \
      ./vendor/bin/phpstan analyse --memory-limit=1G && \
      php artisan test --parallel --processes=8"

build-and-push:
  image: docker:latest
  stage: build
  variables:
    DOCKER_TLS_CERTDIR: "/certs"
  services:
    - docker:dind
  before_script:
    - docker login -u "${CI_REGISTRY_USER}" -p "${CI_REGISTRY_PASSWORD}" "${CI_REGISTRY}"
  script:
    - docker build -f docker/php/Dockerfile --progress=plain -t exmple/api:latest .
    - docker push exmple/api:latest

各ジョブについて

.setup_db

別リポジトリからマイグレーションのイメージを取得し実行するテンプレート。後続のテストジョブ内servicesで定義したpostgresコンテナへマイグレーションを実行。

test

Dockerfileを使用しイメージをビルド後、docker runでテスト実行。

build-and-push

Dockerfileを使用しイメージをビルド後、DockerHubへプッシュ。

問題点

パイプラインの実行時間が長い。通常の開発時はテスト前のビルドのみ実行していたため8分ほど、リリース時にDockerHubへのbuild&pushを行うと14分ほどかかる。理由を以下に整理。

vendor/がキャッシュできない

default:
  image: php:latest
  cache:  # Cache libraries in between jobs
    key: $CI_COMMIT_REF_SLUG
    paths:
      - vendor/
  before_script:
    # Install and run Composer
    - curl --show-error --silent "https://getcomposer.org/installer" | php
    - php composer.phar install

test:
  script:
    - vendor/bin/phpunit --configuration phpunit.xml --coverage-text --colors=never

公式マニュアルではジョブのimageにPHPイメージを指定し、cacheによってvendorディレクトリをキャッシュしている。
しかし、現状構成のdocker runでcomposer installを実行するとcacheは利用できない。そのためパイプラインが実行されるたびにcomposer installが実行され時間がかかる。

https://docs.gitlab.com/ee/ci/caching/#cache-php-dependencies

ビルドを2回実行

テストとDockerhubへのbuild&pushで2回ビルドを行っている。その上キャッシュも利用していないため時間がかかる。
今回の例では一度のビルドで5分近くかかっていたため、ビルドだけで10分要していた。

以前はテスト時のビルドを行わず、ジョブのimage:にPHPを指定していた。しかし、テスト環境構築のためにPHPコンテナ内で「Dockerfileに記載のパッケージとほぼ同様のパッケージ」をインストールする必要がある。パッケージ追加or削除時の際にDockerfileと.gitlab-ci.ymlを編集しなければならず面倒だったため、現在の形となった。

対応結果

上記の問題点を改善するために以下のように修正決定。パイプラインは以下の流れ。

stages:
  - setup
  - build
  - test
  - push

# DBマイグレーションを実行し結果をダンプ、後続のtestジョブへartifactとして渡す
db_setup:
  stage: setup
  image: docker:latest
  services:
    - name: postgres:15.2-alpine3.17
      alias: postgres
    - name: docker:dind
      alias: dind
  script:
    - |
      docker run --network=host --rm \
        -e POSTGRES_ADDRESS="postgres" \
        -e POSTGRES_PORT="${POSTGRES_PORT}" \
        -e POSTGRES_USERNAME="${POSTGRES_USER}" \
        -e POSTGRES_PASSWORD="${POSTGRES_PASSWORD}" \
        -e POSTGRES_DBNAME="${DBNAME}" \
        example/migration:latest
    # pg_dumpでダンプするためパッケージを追加
    - apk add --no-cache postgresql-client
    # マイグレーション内容をダンプ
    - PGPASSWORD="${POSTGRES_PASSWORD}" pg_dump -h postgres -U "${POSTGRES_USER}" -d "${DBNAME}" > test_db.sql
  artifacts:
    # ダンプファイルを1時間保持
    paths:
      - test_db.sql
    expire_in: 1 hour

build:
  image: docker:latest
  services:
    - name: docker:dind
      alias: dind
  variables:
    DOCKER_FILE: ".infrastructure/Dockerfile"
    DOCKER_TLS_CERTDIR: "/certs"
  before_script:
    - docker login -u "${CI_REGISTRY_USER}" -p "${CI_REGISTRY_PASSWORD}" "${CI_REGISTRY}"
  script:
    - echo exmple/api:latest
    # キャッシュとして利用するイメージをpull、失敗しても処理を続ける
    - docker pull exmple/api:latest || true
    - docker buildx build --build-arg BUILDKIT_INLINE_CACHE=1 --cache-from exmple/api:latest -f "${DOCKER_FILE}" -t exmple/api:"${CI_COMMIT_SHORT_SHA}" .
    - docker push exmple/api:"${CI_COMMIT_SHORT_SHA}"

test:
  stage: test
  image: "exmple/api:$CI_COMMIT_SHORT_SHA"
  services:
    - name: postgres:15.2-alpine3.17
      alias: postgres
  dependencies:
    - migration
  script:
    # DB作成
    - PGPASSWORD="${POSTGRES_PASSWORD}" psql -h postgres -U "${POSTGRES_USER}" -c "CREATE DATABASE ${DBNAME};"
    # migrationジョブでダンプしたマイグレーションを適用
    - PGPASSWORD="${POSTGRES_PASSWORD}" psql -h postgres -U "${POSTGRES_USER}" -d "${DBNAME}" < test_job.sql
    # CIのテスト実行時のみDBの接続設定を上書き
    - sed -i "/^POSTGRES_ADDRESS=/c\POSTGRES_ADDRESS=${POSTGRES_ADDRESS}" .env.testing
    # 依存関係のインストール
    - composer install
    # 静的解析
    - ./vendor/bin/phpstan analyse --memory-limit=1G
    # 単体テスト、APIテスト、OpenAPIテスト実行
    - php artisan test --parallel --processes=8
  cache:
    key:
      # composer.lockに変更がない場合は同一のキャッシュが利用される
      files:
        - composer.lock
    paths:
      - vendor/
  tags:
    - docker

push:
  image: docker:latest
  services:
    - name: docker:dind
      alias: dind
  variables:
    DOCKER_TLS_CERTDIR: "/certs"
  before_script:
    - docker login -u "${CI_REGISTRY_USER}" -p "${CI_REGISTRY_PASSWORD}" "${CI_REGISTRY}"
  script:
    # テスト前にbuildしたイメージをpull
    - docker pull exmple/api:"${CI_COMMIT_SHORT_SHA}"
    # タグ付けしてpush
    - docker tag exmple/api:"${CI_COMMIT_SHORT_SHA}" exmple/api:latest
    - docker push exmple/api:latest


build&pushジョブを分割

build->test->pushに分割する。

最初のbuildではCI_COMMIT_SHORT_SHAを使用しハッシュをタグにつけてプッシュ。testジョブのimageにプッシュしたイメージを指定、テストが通った場合はイメージをプルし、latest等のタグを付け再度プッシュ。

このように分割することでイメージビルドを一度で済ませる。

testジョブのイメージ変更

dockerイメージを使用しdocker runで実行していたテストについて、上記のbuildジョブでプッシュしたイメージを使用する形へ変更。

composer installを実行後のvendor/をキャッシュすることが可能。

効果

CIの実行時間の大幅な短縮につながった。特にリリース作業時のパイプライン実行時間が10分ほど短縮された。

検討・調査内容

servicesはジョブ間で共有不可

当初は以下のようにservicesをジョブ間で共有できないかと思ったが不可能。

db_setup:
  stage: setup
  image: docker:latest
  services:
    - name: postgres:15.2-alpine3.17
      alias: postgres

(略)

test:
  stage: test
  image: "exmple/api:$CI_COMMIT_SHORT_SHA"
  services:
    - name: postgres:15.2-alpine3.17
      alias: postgres

db_setupジョブをテンプレート化しtestジョブで使用することも考えたが、その場合はtestジョブでdockerイメージを使用しなければならない。それでは既存と変わらず、vendor/がキャッシュできない。

そのため、

  • db_setupジョブでpostgresをserviceとして使用しマイグレーションを実行
  • ダンプデータをartifact経由でtestで渡す
  • testジョブで新たなpostgresをservicesとして使用、ダンプを適用

という形をとった。

FF_NETWORK_PER_BUILDとnetwork=host

servicesのpostgresへdocker runで実行したコンテナから接続する際に設定が必要。network=hostも必要。いずれもマニュアル参照。

access-service:
  stage: build
  image: docker:20.10.16
  services:
    - docker:dind                    # necessary for docker run
    - tutum/wordpress:latest
  variables:
    FF_NETWORK_PER_BUILD: "true"     # activate container-to-container networking
  script: |
    docker run --rm --name curl \
      --volume  "$(pwd)":"$(pwd)"    \
      --workdir "$(pwd)"             \
      --network=host                 \
      curlimages/curl:7.74.0 curl "http://tutum-wordpress"

https://docs.gitlab.com/ee/ci/services/?utm_source=chatgpt.com#using-services-with-docker-run-docker-in-docker-side-by-side

プライベートDockerHubレジストリのイメージpull

DOCKER_AUTH_CONFIGをCI/CD variablesに登録する必要がある。

パイプラインで使用する認証情報でdocker login。($CI_REGISTRY_PASSWORDなど)
~/.docker/config.jsonの中身を取得し登録。

https://docs.gitlab.com/ee/ci/docker/using_docker_images.html#access-an-image-from-a-private-container-registry

buildジョブで使用するキャッシュイメージのタグ

latestを指定しキャッシュに使用。

$CI_COMMIT_REF_SLUGを使用しブランチ毎の最新イメージをキャッシュして使用するか迷ったが、releaseやfeatureの一発目はキャッシュが効かないので見送り。

main、release、feature関係なくlatest。そもそもDockerfileへの変更はそこまで頻繁に発生しないため。

その他調査

Docker-in-Docker

image: docker:latest

のように、イメージの指定だけではdockerコマンドを使用できない。dindサービスも含める必要がある。
https://gitlab-docs.creationline.com/ee/ci/docker/using_docker_build.html

まとめ

LaravelのパイプラインをGitLab CIで構築する記事が少なすぎる。公式も古いし。
あと、本当はbuildジョブでビルドしたイメージをcacheとかartifactとかで渡したい。パイプラインの途中成果物をレジストリにあげたくない。パイプライン内で完結したい。

GitHubで編集を提案

Discussion

Hiroshi KoyamaHiroshi Koyama

ユースケースが一致するかわかりませんが、Docker イメージの pull や push を省略可したい場合は、GitLab Runner を(クラウドサービスではなく)ローカルマシンで用意して利用すると良いかもしれません。

自分で用意した GitLab Runner の設定ファイルである config.toml で次の指定をすると、プルの指定に if-not-present が可能となり、GitLab Runner がキャッシュしたイメージが使われるようになります。

[[runners]]
  (略)
  [runners.docker]
  (略)
    allowed_pull_policies = ["always", "if-not-present"]