ENECHANGE Developer Blog

ENECHANGE開発者ブログ

Circle CI から buildkite に移行し、コスト抑制しつつテスト実行時間を短縮した話

ENECHANGE チーフエンジニアの cuzic こと、川西です。

運動不足の解消のために、朝に筋肉体操をやり始めました。 他の種目はそこまででもないのに、腕立て伏せだけ、異常にきつい。

自分に甘えない!最後までやりきる!という気持ちで日々続けています。

はじめに

ENECHANGE では GitHub に push したら、自動的に Circle CI でテストが走るように設定しています。運用上、この自動テストの成功をリリースの必須条件としています。 本番環境で不具合が生じることを未然に防ぎ、サービス品質を維持しています。

しかしながら、プロジェクト数の増加、開発するエンジニア数の増加などにともない、Circle CI の待ち時間が長くなりすぎる点が問題になってきました。

Circle CI の Performance Plan と Buildkite

Circle CI では Performance Plan という料金体系があります。この新しい料金体系を採用すると、同時並行の実行数を増やすことができ、テストの待ち時間を短縮できます。 ENECHANGE は、昔から Circle CI を使っているため Performance Plan に変更される前のプランを採用していました。当然、この Performance Plan に移行する選択肢もありました。

Performance Plan への移行も詳細に検討したのですが、そうはせず新たに Buildkite という CI ツールを導入することにしました。

Buildkite は、「テストの実行環境を自分で構築する」というのがとても特徴的な CI サービスです。自分で構築するといっても実際には カンタンに構築するための CloudFormation の設定 が存在するため、そこまで難しくはありません。

詳細は割愛いたしますが、Circle CI を Performance Plan に移行するよりも、Buildkite を導入する方が、月間 1,000 USD ほど安価に、テストの高速化、待ち時間の短縮が実現できるという試算結果となりました。コストメリットが非常に大きい点を重視し、Buildkite を導入することにいたしました。

この記事で説明すること

最初、もっとステップバイステップで、Buildkite の導入手順を説明しようかな、と考えたのですが、そうするととてもとても長い記事になってしまうことに気が付きました。

そこで、この記事ではそうはせず、公式ドキュメントの内容に加えて、これを先に知っていたら良かったと、自分で思うようなことをピックアップして、紹介します。

Docker Compose を利用した Buildkite 実行環境の構築

今回、enechange では Buildkite を Docker Compose と組合わせて利用しています。

テストを並列実行する場合、データベースをそのテスト実行環境ごとに用意する必要があります。 Docker Compose を使うとテスト実行環境ごとにデータベースを構築することが、とてもカンタンにできます。

Docker Compose を利用するための Buildkite プラグイン もあります。

この Buildkite プラグインでは、

  • ホスト側で Buildkite エージェントがインストール、動作している
  • ホスト側で GitHub のリポジトリをチェックアウトする
  • ジョブのパイプライン設定の中で Docker Compose を使うように設定する
  • ジョブのパイプライン設定のとおり、Docker コンテナ内でテストが実行される

というように動作しています。

buildkite の実行モデル

Buildkite ではエージェントがキューを監視し、新しいジョブがあればそのジョブを実行します。 Elastic CI Stack を利用する場合は、エージェントがインストール済、設定済の AMI を利用するため、エージェントのインストールを意識する必要はありません。

Buildkite は Share nothing 、State Free という実行モデルを採用しています。

Buildkite では、実行パイプラインを複数から構成したとき、前段のステップと後段のステップは別のサーバで実行される場合があります。 前段のステップでの出力結果を後段で利用する必要がある場合は、別の場所にアップロードした上で、後段ではそれをダウンロードする必要があります。 この点、少し面倒ですが、 Share nothing 、State Free という実行モデルであるがゆえにスケールアウトが容易になります。 共通の事前準備の作業は 1台のサーバで実行し、時間がかかるテストの実行についてだけ多数のサーバで実行することが可能です。

例えば、前段では1台のサーバで bundle install を行い、後段では10台でテストを実行することが可能です。

サーバリソースを最大限に有効活用でき、サーバコストを削減できます。

AWS 上で Buildkite の実行環境を構築

AWS 上で Buildkite の実行環境を構築するときは、 Elastic CI Stack for AWS guide の記事の内容がとても参考になります。

この記事の手順にしたがって、一度、環境構築してみるのがオススメです。

enechange では最終的にはこの手順ではなく、elastic-ci-stack-for-aws のリポジトリを clone した上で config.json 等を独自に設定しました。 そうすることで、AWS インフラの設計をコードで管理でき、 Infrastructure as a Code を実践できます。再現性があり、修正・レビューなどもカンタンにできるため、安心感があります。

実施手順としては、

  1. GitHub の Launch Stack ボタンを押して、適切な設定を入力し、環境を構築する。
  2. 一通り、Buildkite でテストが成功できることを確認する。
  3. 設定内容のメモを残す。

というのを一度やってみてから、その設定内容のメモを残した上で、Elastic CI Stack for AWS のドキュメントを参考にしながら、 config.json ファイルを作成するのがいいでしょう。

Buildkite で発行されるアクセストークンなどのパラメータを config.json で設定するのですが、 パラメータの意味を一通り理解してから設定する方が効率的です。

Buidlkite の pipeline.yml の作成

Buildkite では .buildkite/pipeline.yml という設定ファイルで、テスト環境で何をするかを設定します。 Circle CI における .circleci/config.yml に相当します。

pipeline.yml での設定で特に重要なポイントを紹介します。

GitHub 上の private リポジトリへのアクセス

GiHhub の private リポジトリをクローンするとき、SSH のプライベートキーが必要となります。 Elastic CI Stack を使うと、 ジョブを実行するときに、AWS S3 に保管している SSH キー等を自動的に取得するようになっています。SSH のプライベートキーは非常に機微な情報ですので、AWS KMS の暗号化を利用して S3 上に保管することが推奨されています。

そのための具体的な手順は Build Secrets での記述内容に記載があります。

適切に設定することで、GitHub の private リポジトリの内容をクローンすることができます。

各種環境変数の保管

Buildkite では環境変数は複数の方法で設定できます。 - パイプラインの設定画面 - environment hook というエージェント実行時に実行される最初のフック - Elastic CI Stack を利用している場合、ここで S3 上の env ファイルが読み込まれる - pipeline.yml の設定ファイル内 - トップレベルの env 属性 - 各ステップ内の個別の env 属性

これら設定はエージェントが実行されるホスト OS でジョブを実行するときの環境変数の話である点に留意する必要があります。 Docker で実行されるコンテナ内の環境変数については docker-compose.yml で別に設定する必要があります。

Buildkite での環境変数の管理の方針

下記の指針に従って、環境変数の設定方法を使い分けることにしました。

  • pipeline.yml 内で同一の設定を複数回、記述する必要がある場合
    • pipeline.yml のトップレベルの env 属性で設定し、DRY にする
    • pipeline.yml の各ステップで個別の env 属性は使わない
  • コンテナ内でのみ必要な環境変数は docker-compose.yml の env_file で設定する
    • ただし、機微なシークレット情報については、S3 内に保管する
  • 上記以外の場所では環境変数を設定しない

Buildkite のドキュメントでも Managing Pipeline Secrets でシークレット情報の管理について アンチパターンを中心にいろいろと紹介されています。

下記の3つの方法のいずれかを採用しましょう。

enechange ではこのうち、 Elastic CI Stack が提供する方法である S3 上の env ファイルに保存する方法で、シークレット情報を管理することにしました。Elastic CI Stack のドキュメントで推奨されている方法で、カンタンに導入できます。

docker-compose.yml で工夫した点

buildkite と docker-compose を組み合わせて使う場合、特に気を付けるべき点はホスト上の環境変数を Docker コンテナ内に受け渡しすることでしょう。

具体的には、下記の環境変数を Docker コンテナ内でも使えるように設定しました。

AWS コマンドを Docker 内で利用するときに必要となる環境変数、 後述する Knapsack gem を使う上で必要となる Buildkite 関連の環境変数を Docker コンテナ内に記述しています。

services:
  web:
    environment:
      # AWS credentials
      AWS_ACCESS_KEY_ID:
      AWS_SECRET_ACCESS_KEY:
      AWS_SESSION_TOKEN:
      # CI envs
      CI:
      RAILS_ENV:
      # BUILDKITE_ENVS
      BUILDKITE_BRANCH:
      BUILDKITE_BUILD_CHECKOUT_PATH:
      BUILDKITE_BUILD_ID:
      BUILDKITE_BUILD_NUMBER:
      BUILDKITE_COMMIT:
      BUILDKITE_PARALLEL_JOB:
      BUILDKITE_PARALLEL_JOB_COUNT:
      BUILDKITE_RETRY_COUNT:

Knapsack gem の利用

Buildkite は Knapsack gem と組み合わせて利用することが推奨されているようです。 そのため enechange でも Knapsack gem を利用することにしました。

Knapsack gem の導入手順 は公式ドキュメントでの説明が充実しています。

ポイントとしては、ローカルで

KNAPSACK_GENERATE_REPORT=true  bundle exec rspec spec

を実行した上で、生成された knapsack_rspec_report.json を git にコミットしておくことです。

そうすることで、そのときの実行時間に応じて適切に分割してテストを並列実行することができます。 Knapsack gem が便利な点は、 Buildkite が自動的に設定する環境変数に従って、並列実行してくれる点です。 Knapsack gem のためにわざわざ環境変数を設定する必要がなく、ラクに導入することができます。

並列実行

下記は Buildkite のジョブのパイプライン設定の例です。

env:
  docker_compose_config: rails/docker-compose.buildkite.yml
  docker_repository: 0123467890123.dkr.ecr.ap-northeast-1.amazonaws.com/enechange
  aws_account_id: "0123467890123"
  BUILDKITE_PLUGIN_DOCKER_COMPOSE_CONFIG: rails/docker-compose.buildkite.yml
  BUILDKITE_PLUGIN_ECR_LOGIN: true
  BUILDKITE_PLUGIN_ECR_NO_INCLUDE_EMAIL: true
  BUILDKITE_PLUGIN_ECR_REGION: "ap-northeast-1"

steps:
  - label: "db:migrate and test %n"
    key: "test"
    commands:
      - bundle install
      - ( cd frontend; yarn install --ignore-engines; yarn release )
      - bundle exec rails db:schema:load
      - bundle exec rake knapsack:rspec
    retry:
      automatic:
        - exit_status: 1
          limit: 4
    parallelism: 9
    plugins:
      - docker-compose#v3.3.0:
          build: web
          image-repository: $docker_repository
      - ecr#v2.1.0:
          account_ids: $aws_account_id

aws_account_id や docker_repository の環境変数は各自で適切な値を設定してください。

この中で特に parallelism: 9 が重要です。 この設定により 9 並列でテストを実行することができます。

終わりに

今回の記事では、buildkite 導入に至る経緯と導入手順について説明しました。

buildkite を利用することで、テストを大幅に高速化できる上、コストも抑制できます。

導入を検討している方の一助となればと思います。