FBZにおけるCI/CDパイプライン改善の取り組み

FBZにおけるCI/CDパイプライン改善の取り組み

はじめに

こんにちは、ZOZOMO部OMOバックエンドブロックの杉田です。普段はFulfillment by ZOZO(以下、FBZ)が提供するAPIシステムを開発・運用しています。

FBZでは、昨年からビルドの高速化や自動デプロイをはじめとしたCI/CDパイプラインの最適化に取り組んできました。本記事では、それらの取り組みの詳細とその効果についてご紹介します。

目次

FBZにおけるCI/CDと構成管理の現状

FBZでは、GitHub Actions(以下、GHA)とAWS CodeBuild(以下、CodeBuild)を用いてCI/CDパイプラインを構築しています。

利用用途などは、それぞれ以下の表の通りです。

GHA CodeBuild
主な利用用途 ・ユニットテスト
・静的コード解析
・カバレッジ計測
・アプリケーションのビルド
・AWSへのデプロイ
・E2Eテスト
トリガー ・プルリクエストへのPush ・手動実行
設定管理 ・コードベース ・手動管理

また、FBZはサーバーレスアーキテクチャを採用しており、AWS Lambda(以下、Lambda)を中心としたAWSが提供しているフルマネージドサービスを中心に構築されています。サービス構成やアーキテクチャ戦略の詳細については、以下の記事をご参照ください。

techblog.zozo.com

構成管理ツールとしては、サーバーレスアーキテクチャと相性の良いServerless Frameworkを採用しています。

CodeBuildの処理概要

FBZでは、管理対象のリソースが多いことから関心事毎に定義ファイルを分割しています。そして、分割された定義ファイルはCodeBuildから直列実行されることで、デプロイ対象となるスタックの状態を最新化していきます。これらの構成はFBZの開発当初からあまり変わっておらず、大きな課題に直面することもなく、最近まで開発してきました。
一方で、リリースサイクルに関してはここ数年で大幅な見直しを行いました。

リリースサイクルの見直し

以前までは、隔週水曜日をリリース日としており、ある程度まとまった量の修正内容を一度にリリースするリリースサイクルを採用していました。

しかし、昨年からユーザーへの価値提供の速度を向上させることを目的として、リリースサイクルの見直しが行われました。その結果、リリース可能な状態になった修正は、可能な限り早いタイミングでリリースするという、リリース日を固定しないリリースサイクルへと変わりました。
この変化により開発者にも以下の利点がありました。

  • ビックバンリリースがほとんど行われなくなり、精神的な安定感が得られた
  • 万が一リリースに伴う障害が発生しても、原因の調査や特定が容易になった

リリースまでの流れ

リリースサイクルを早く回していくという体制になりましたが、開発着手からリリースまでの流れは以前とあまり変わっていません。

以下の図は、FBZ開発におけるリリースまでの一連の流れになります。 開発着手からリリースまでの流れ

この図で注目していただきたいのは、AWS上で実施される「検証・リリース」の2つです。

「設計・実装/テスト・レビュー」については、通常、開発者やレビュアのローカル環境で完結します。これらの3つのタスクにおいて待ち時間は、ユニットテストの実行中に数分程度という限定的なものです。
しかし、「検証・リリース」に関しては、AWS上へのデプロイを伴うため、デプロイが完了するまでに1時間以上もの待ち時間が発生します。検証にデプロイを伴う理由は、Lambdaベースのサーバーレスアーキテクチャを使用しており1、AWS上のLambdaに修正したコードを反映させないと動作確認できないことが挙げられます。

顕在化した課題

リリースサイクルを早くしたことでリリースする機会が増え、必然的にデプロイ回数も増えました。
その結果、次のような課題が見えてきました。

  • 長時間のデプロイ
  • ビルド環境のメンテナンス性の低さ
  • 手動デプロイが抱える人為的なリスク

長時間のデプロイ

前述した通り、FBZでは開発当初に構築したCI/CDパイプラインを使ってきましたが、FBZは開発開始から6年以上が経過しています。日々、開発・保守を続けてきたことでアプリケーションコードをはじめ、サービス構成も複雑かつ肥大化してきました。その結果、開発時間のうち、リリースや検証作業といったデプロイを伴う作業にかかる時間の割合が増えてしまいました。

FBZではリリースで問題が発生した際に、再デプロイによって切り戻しを行うことがあります。そのため、デプロイに時間がかかってしまうと、それだけサービスの信頼性にも影響が出てしまいます。

ビルド環境のメンテナンス性の低さ

CodeBuild上に作成するビルドプロジェクトと呼ばれる環境の中では、以下の項目をはじめとする様々な設定ができます。

  • メモリやCPUのスペック
  • ビルド対象のソースコード
  • 環境変数

しかし、これらの項目をメンテナンスする上で、次のような課題がありました。

  • 設定が手動で追加・更新されていた
  • 変更内容のレビューが困難であった
  • 変更履歴が追跡できなかった

これらの課題もあり、従来のビルド環境はメンテナンスし易いとは言いづらい状態でした。

手動デプロイが抱える人為的なリスク

手動によるリリースは手間がかかるだけでなく、人為的な問題を起こしてしまうリスクがあります。実際に、FBZではCodeBuild上でデプロイ対象のブランチ名の入力や環境変数を更新する場合など、リリース作業中は常にWチェックしながら操作に誤りが無いかを目視で確認していました。

CI/CDパイプラインの改善に向けて

先程までの課題を整理すると、いずれもCI/CDパイプラインを改善することで解決できることが分かりました。
以降は、それぞれの課題に対して取り組んだ内容を紹介していきます。

  • デプロイフローの見直し
  • CodeBuildのバッチビルド
  • ビルド定義の実装例

デプロイフローの見直し

デプロイに時間がかかっていた要因を分析したところ、複数のServerless Framework定義ファイルを、単一のビルドプロジェクト内部で直列実行していたことが原因と分かりました。そこで、直列実行していたデプロイ処理を並列実行させる方法として、CodeBuildのバッチビルドという機能に注目しました。

CodeBuildのバッチビルド

CodeBuildは、いくつかの種類のバッチビルドをサポートしています。
バッチビルドに関する詳細は、以下のドキュメントを御覧ください。

docs.aws.amazon.com

FBZではビルドグラフという機能を利用しました。
ビルドグラフでは、タスク同士の依存関係を定義し、その定義された依存関係に基づいてビルドが実行できます。また、定義の仕方によって複数のタスクを並列実行させることもできます。

タスク定義の実装例

FBZのビルド構成を参考として、タスク定義の方法を紹介していきます。
今回は、以下の依存関係を持つタスク定義を実装していきます。

ビルドグラフによるビルド定義例

定義ファイル(buildspec)は以下のようになります。

# buildspec.yml

version: 0.2

batch:
  build-graph:
    # PRE BUILD
    - identifier: PRE_BUILD_1
      buildspec: buildspec_pre_build_1.yml

    # BUILD
    # 共通のbuildspecを使い、タスク毎に環境変数でデプロイ対象を制御
    - identifier: API_BUILD_1
      buildspec: buildspec_build.yml
      env:
        variables:
          DEPLOY_TARGET: api_a
      depend-on:
        - PRE_BUILD_1

    - identifier: API_BUILD_2
      buildspec: buildspec_build.yml
      env:
        variables:
          DEPLOY_TARGET: api_b
      depend-on:
        - PRE_BUILD_1

    - identifier: BATCH_BUILD_1
      buildspec: buildspec_build.yml
      env:
        variables:
          DEPLOY_TARGET: batch_a
      depend-on:
        - PRE_BUILD_1

    # POST BUILD
    - identifier: POST_BUILD_1
      buildspec: buildspec_post_build_1.yml
      depend-on:
        - API_BUILD_1
        - API_BUILD_2

    - identifier: POST_BUILD_2
      buildspec: buildspec_post_build_2.yml
      depend-on:
        - API_BUILD_1
        - API_BUILD_2
        - BATCH_BUILD_1

特徴として、メインとなるタスク(API_BUILD_1API_BUILD_2BATCH_BUILD_1)ではbuildspecを共通化していることが挙げられます。
タスクごとに環境変数を注入することで、ビルド処理を制御できるようにしています。

    - identifier: BUILD_n
      buildspec: buildspec_build.yml
      env:
        variables:
          DEPLOY_TARGET: xxxxxxxx # 任意の値

こうすることで、今後さらにビルド対象が増えたとしても、環境変数を変えるだけでタスクの追加が行えるので、複雑になりがちなタスク定義が冗長にならず保守しやすくなります。

定義ファイルの詳細は以下のドキュメントを御覧ください。

docs.aws.amazon.com

個々のタスクを高速化

Serverless Frameworkのオプションを見直すことで、各タスクのデプロイ時間短縮を図りました。

  • Direct deploymentsの有効化
  • devDependenciesの依存解決

Direct deploymentsの有効化

CloudFormationのスタック作成時に変更セットを作成しないことで、デプロイ時間の高速化を実現する設定があります。以下のように定義します。

# serverless.yml

provider:
  deploymentMethod: direct

なお、以下のドキュメントにある通り、次期バージョンであるServerless Framework v4からは上記の設定がデフォルトとなるようです。

www.serverless.com

今後も変更セットを利用したい場合は、以下の定義を明示的に記述することで、Serverless Framework v3までと同じ設定でデプロイが可能です。

# serverless.yml

provider:
  deploymentMethod: changesets

deploymentMethod: directとした場合のCloudFormationの挙動については、以下のドキュメントを御覧ください。

docs.aws.amazon.com

devDependenciesの依存解決

デプロイパッケージ作成時に、devDependenciesの依存解決の除外処理に時間がかかっていたので、それらの処理を実行させないことで時間短縮を図りました。定義は以下のとおりです。

# serverless.yml

package:
  excludeDevDependencies: false

詳細はドキュメントを御覧ください。

www.serverless.com

デプロイフローを改善した効果

今まで紹介した改善策の実施前後で、ビルド時間にどれだけ変化があったかをキャプチャした結果が以下の図です。

改善前のビルド時間: 1時間14分
改善後のビルド時間: 24分

改善後はバッチビルドを利用しているため、改善前と比べるとフェーズの内訳が異なりますが、改善後のIN_PROGRESSで改善前の全フェーズを並列で実行しているイメージになります。

ビルドの並列化とServerless Frameworkのオプション見直しを行ったことで、およそ70%程度のデプロイ時間短縮を実現できました。

手動運用からの卒業

今まで手動で行ってきたCodeBuildのメンテナンスやデプロイですが、GitHubへの操作をトリガーに、各操作が自動で実行される仕組みを構築しました。

GitHub Actionsの活用

FBZではリリース毎にGitHub上でタグを付与しています。CodeBuildでのデプロイ時にはそのタグをデプロイ対象として使用しています。

タグの生成

タグの生成には、以下のアクションを利用しています。

github.com

GHAのワークフローは以下の通りです。

name: Release Drafter

on:
  push:
    branches:
      - main

jobs:
  create-draft-release:
    name: Create Draft Release.
    runs-on: ubuntu-latest
    steps:
      - uses: release-drafter/release-drafter@v5
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

挙動を図化したものが以下になります。mainブランチへのpushをトリガーにアクションが起動してタグを生成します。

タグの運用ルール

ビルド環境の自動アップデート

CodeBuildで管理しているビルドプロジェクトに対して、変更履歴の追跡や事前のレビューを可能にするため、CodeBuildのリソース定義をコード化して管理することにしました。コード化するにあたり、CloudFormationのテンプレートを利用しました。
スタックの更新は、以下GHAのワークフローを用いて実現しています。

on:
  push:
    tags:
      - '*'

env:
  STACK_NAME: your_stack_name
  AWS_REGION: your_aws_region

jobs:
  update-build-project:
    name: Update CodeBuild Stack.
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
      issues: write
      pull-requests: write

    steps:
      - uses: actions/checkout@v3

      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v2
        with:
          role-to-assume: arn:aws:iam::123456789012:role/xxxxxxxxxxxxxxxxxx
          aws-region: {{ env.AWS_REGION }}

      # テンプレートのデプロイと、削除保護の有効化
      - name: Update Codebuild Build Project
        run: |
          aws cloudformation deploy \
            --role-arn "arn:aws:iam::123456789012:role/xxxxxxxxxxxxxxxxxx" \
            --template "your_template_name.yml" \
            --stack-name ${{ env.STACK_NAME }} \
            --capabilities CAPABILITY_IAM
          aws cloudformation update-termination-protection \
            --enable-termination-protection \
            --stack-name ${{ env.STACK_NAME }}

上記ワークフローでは、CloudFormationのテンプレートに基づいて、対象のスタックに差分がある場合にのみ更新が行われます。
スタックに差分がない場合は、以下の画像のように更新は行われず処理が終わります。

ワークフローのログ

これによって、CodeBuildの設定変更の履歴をGitHubで管理できるようになったほか、CI上で自動的にスタックの最新化が行われるようになりました。

自動デプロイ

先程までのワークフローに、デプロイを行うためのJobを追加します。

# (中略)

env:
  STACK_NAME: your_stack_name
  AWS_REGION: your_aws_region
  # 追加
  PROJECT_NAME: your_project_name

jobs:
  # update-build-project:
  #     (中略)

  deployment:
    name: Deploy.
    needs: update-build-project
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
      issues: write
      pull-requests: write
    steps:
      - uses: actions/checkout@v3

      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v2
        with:
          role-to-assume: arn:aws:iam::123456789012:role/xxxxxxxxxxxxxxxxxx
          aws-region: {{ env.AWS_REGION }}

      - name: Start CodeBuild Batch Build
        run: |
          aws codebuild start-build-batch \
            --project-name ${{ env.PROJECT_NAME }} \
            --source-version ${{ github.sha }}

ワークフロー内部でやっていることはシンプルで、AWS CLIを実行することでビルドプロジェクトのデプロイを開始させています。ここで追加したJobは、needs: update-build-projectを指定することで、ビルドプロジェクトの最新化が終わり次第起動します。

  deployment:
    name: Deploy.
    needs: update-build-project

ここまで、タグの生成をトリガーとした前提でワークフローの紹介をしてきましたが、タグの生成以外にも様々なイベントをトリガーにワークフローを実行させることができます。

詳細はドキュメントを御覧ください。

docs.github.com

新生CI/CDパイプライン

ここまでの改善によって、今まで手動で行っていたデプロイに関係する操作が、タグの生成をトリガーとしてGHAのワークフローから自動実行されるようになりました。また、デプロイも直列実行から依存関係に従って並列実行されるようになりました。

改善前のデプロイフロー
改善後のデプロイフロー

さいごに

本記事では、FBZにおけるCI/CDパイプラインの最適化させる取り組みと、それらの効果についてご紹介しました。

CI/CDパイプラインの最適化を実施した結果、以下の恩恵を得ることができました。

  • ビルドを並列化したことでデプロイ時間短縮
    • 開発中の待ち時間が減って開発サイクルが高速化された
    • 問題発生時の復旧にかかる時間が早くなった
  • 手動で行っていた設定や操作の自動化
    • 定義をコード化して管理できるようになったことで、変更追跡ができるようになった
    • ビルド環境のメンテナンスが楽になった
    • 変更内容がレビュー可能になった

CodeBuildやServerless Frameworkを利用している方や、GHAを使ったCI環境の構築に興味がある方はぜひ参考にしてみてください。

ZOZOMO部では、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。

hrmos.co


  1. Serverless Frameworkには任意のLambdaのみをデプロイする機能がありますが、CloudFormation スタックの管理対象外となり、リソース管理が煩雑になるためFBZでは利用していません
カテゴリー