GitLab CI/CD 上級編:パイプラインの最適化と継続的デプロイ戦略

Development tutorial - IT technology blog
Development tutorial - IT technology blog

遅いパイプライン、怖いデプロイ——誰もが抱える悩み

以前、5人チームのWebアプリプロジェクトに関わっていたことがある。GitLab CI/CDのパイプラインが毎回のプッシュで18〜22分かかっていた。誰もマージもデプロイも嫌がっていた——長時間待った挙句、最後のステップで失敗するのが怖かったからだ。フィーチャーブランチは1週間以上生き続け、マージコンフリクトが積み重なり、スプリントのたびにリリースが悪夢と化していた。

問題はGitLabにあったのではない。パイプラインの設定方法にあった。.gitlab-ci.ymlをリファクタリングして適切な戦略を適用した結果、パイプラインは6〜7分に短縮された。チームはスプリント末にまとめてマージするのではなく、毎日マージするようになった。この記事では、実際にやったことを共有する。

GitLab CI/CDの仕組みを正しく理解する

間違った箇所を最適化すると、何もしないよりかえって悪化する。設定に手を入れる前に、GitLab CI/CDの実行モデルをしっかり把握しておこう。

Stage・Job・Pipeline

GitLabはパイプラインをステージ単位で実行する。同じステージのジョブは並列に動き、ステージ同士は順次実行される。これを覚えておくことが、ジョブの分割と実行順序を決める上での基本になる。

stages:
  - build
  - test
  - deploy

build-image:
  stage: build
  script:
    - docker build -t myapp:$CI_COMMIT_SHA .

test-unit:
  stage: test
  script:
    - pytest tests/unit/

test-integration:
  stage: test   # test-unitと並列で実行される
  script:
    - pytest tests/integration/

DAG — 有向非巡回グラフ

GitLab 12.2でneeds:が追加され、DAGパイプラインが構築できるようになった。ステージ全体の完了を待つのではなく、依存関係が解決された時点でジョブをすぐに実行できる——ステージのバリアを完全に無視できる。GitHub ActionsでもCI/CD自動化の考え方は共通しており、ゼロから始めるGitHub Actions CI/CDガイドも参考になる。

stages:
  - build
  - test
  - deploy

build-backend:
  stage: build
  script: make build-backend

build-frontend:
  stage: build
  script: make build-frontend

test-backend:
  stage: test
  needs: [build-backend]   # build-frontendの完了を待たなくてよい
  script: make test-backend

test-frontend:
  stage: test
  needs: [build-frontend]  # build-frontendが終わり次第すぐに実行
  script: make test-frontend

deploy-staging:
  stage: deploy
  needs: [test-backend, test-frontend]
  script: make deploy-staging

この構成により、test-backendtest-frontendはお互いを待たずに済む——すぐに数分の節約になる。

パイプライン最適化の実践:AからZまで

1. スマートなキャッシュ——毎回依存関係を再インストールしない

パイプラインが遅くなる最大の原因:依存関係のキャッシュがないこと。ジョブが実行されるたびに、ランナーがnode_modulesやPythonパッケージを一から取得してくる——前日にすでにインストール済みのものに3〜5分を費やすだけだ。

variables:
  PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"

cache:
  key:
    files:
      - requirements.txt     # このファイルが変わるとキャッシュキーも変わる
  paths:
    - .cache/pip
    - venv/

test:
  stage: test
  before_script:
    - python -m venv venv
    - source venv/bin/activate
    - pip install -r requirements.txt
  script:
    - pytest

Node.jsの場合はpackage-lock.jsonをキャッシュキーに使う:

cache:
  key:
    files:
      - package-lock.json
  paths:
    - node_modules/

2. パラレルマトリックス——複数環境でテストを並列実行

各Pythonバージョンを順番にテストする代わりに、parallel:matrixを使おう:

test:
  stage: test
  image: python:$PYTHON_VERSION
  parallel:
    matrix:
      - PYTHON_VERSION: ["3.10", "3.11", "3.12"]
  script:
    - pip install -r requirements.txt
    - pytest --tb=short

3つのジョブが順次ではなく並列で実行される——合計時間が1ジョブ分の時間まで短縮される。

3. only/exceptではなくrulesを使う——ジョブの実行条件を正確に制御

only/exceptはすでに非推奨だ。より明確なロジックのためにrules:を使おう:

deploy-production:
  stage: deploy
  script:
    - make deploy-prod
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH   # mainブランチでのみ実行
      when: manual                                   # 手動でボタンを押す必要がある
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
      when: never                                    # MRパイプラインでは実行しない

4. アーティファクト——ステージ間でファイルを受け渡す

一度ビルドして、複数の場所で使い回す。各ステージでイメージやバイナリを再ビルドするのはやめよう:

build:
  stage: build
  script:
    - make build
    - echo $CI_COMMIT_SHA > build_info.txt
  artifacts:
    paths:
      - dist/
      - build_info.txt
    expire_in: 1 hour   # 1時間後に自動削除してストレージを節約

deploy:
  stage: deploy
  needs:
    - job: build
      artifacts: true   # buildジョブのアーティファクトをダウンロード
  script:
    - cat build_info.txt
    - rsync -av dist/ user@server:/var/www/app/

継続的デプロイの実践的戦略

Blue-Greenデプロイ

考え方:本番環境を2つ維持する(Blueが稼働中、Greenに新バージョンをデプロイ)。Greenが安定したらトラフィックをGreenに切り替える——ダウンタイムゼロ。Webhookで自動デプロイを実現する方法と組み合わせると、手動SSH作業を完全になくせる。

deploy-green:
  stage: deploy
  script:
    - docker pull myapp:$CI_COMMIT_SHA
    - docker stop myapp-green || true
    - docker run -d --name myapp-green -p 8081:8080 myapp:$CI_COMMIT_SHA
    - sleep 10
    - curl -f http://localhost:8081/health || (docker stop myapp-green && exit 1)
  environment:
    name: production-green

switch-traffic:
  stage: switch
  needs: [deploy-green]
  when: manual
  script:
    - nginx -s reload   # またはロードバランサーの設定を更新
    - docker stop myapp-blue || true
    - docker rename myapp-green myapp-blue
  environment:
    name: production

Canaryデプロイ

Canaryデプロイでは、新バージョンに一度に100%のトラフィックを流さない。最初はユーザーの5〜10%だけが新バージョンを受け取る——数時間、エラーレートとレイテンシを監視する。メトリクスが問題なければ全体にロールアウトする。Dockerイメージを軽量に保つことでデプロイ速度がさらに上がるため、Dockerイメージサイズの最適化も合わせて実践したい。

deploy-canary:
  stage: deploy
  script:
    - kubectl set image deployment/myapp-canary app=myapp:$CI_COMMIT_SHA
    - kubectl scale deployment/myapp-canary --replicas=1  # 1/10 pods = トラフィックの10%
  environment:
    name: production/canary
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

promotion-canary:
  stage: promote
  needs: [deploy-canary]
  when: manual
  script:
    # メトリクスを確認後、全体にロールアウト
    - kubectl set image deployment/myapp app=myapp:$CI_COMMIT_SHA
    - kubectl scale deployment/myapp-canary --replicas=0
  environment:
    name: production

環境変数とシークレットの正しい管理

.gitlab-ci.ymlに認証情報をハードコードするのは絶対に避けよう。GitLab CI/CD変数(Settings → CI/CD → Variables)を使うこと。APIキーの管理ミスが本番障害を引き起こした事例についてはAIサービスのAPIキーセキュリティ:真夜中のプロダクション障害から学んだ教訓が参考になる:

deploy:
  script:
    - echo "$DEPLOY_KEY" > /tmp/deploy_key
    - chmod 600 /tmp/deploy_key
    - ssh -i /tmp/deploy_key user@$PROD_SERVER "cd /app && ./deploy.sh"
    - rm /tmp/deploy_key

機密性の高い認証情報には:Protected(プロテクトブランチでのみアクセス可能)とMasked(ログから非表示)を有効にしよう。この2つのチェックは5秒で設定できるが、多くのトラブルを未然に防いでくれる。

Pipeline as Code——大規模プロジェクトのCIファイル分割

.gitlab-ci.ymlが300行を超えて肥大化してきたら、include:を使って複数ファイルに分割しよう:

# .gitlab-ci.yml
include:
  - local: .gitlab/ci/build.yml
  - local: .gitlab/ci/test.yml
  - local: .gitlab/ci/deploy.yml
  - project: 'myorg/ci-templates'  # 別リポジトリのテンプレートを使用
    ref: main
    file: '/templates/docker.yml'

stages:
  - build
  - test
  - deploy

適用後の結果

冒頭で触れたプロジェクトに話を戻そう。効果の80%をもたらしたのは3つのことだった:needs:によるDAG、依存関係のキャッシュ、そして並列テストマトリックス。さらにフィーチャーブランチで不要なジョブを削除した結果、パイプラインは20分から6分に短縮された。チームは毎日マージするようになり、フィーチャーブランチが2日以上生き残ることはほとんどなくなった。

最も大きく変わったのは時間ではなく、デプロイに対するチームの姿勢だった。Blue-Greenによって問題があれば30秒でロールバックできるようになった。「デプロイを間違えた」代償がたった30秒の対応で済むようになると、チームは以前よりずっと積極的にリリースするようになった——2週間に1回から、週に数回へと変化した。

良いパイプラインとは複雑なパイプラインではない——速く動き、失敗が少なく、誰でも読めるパイプラインだ。まずキャッシュと並列ジョブから始めよう。チームが慣れてからBlue-GreenやCanaryを検討すればいい。

Share: