GitLab CIの実行速度を上げる

こんにちは、弓場と申します。

少し前まではKubernetesのyamlファイルをたくさん書いておりましたが、最近はAnsibleのyamlをたくさん書いています。

今回はGitLab Runnerを使う際に直面した問題とそれを解決するために使用したDocker Machine と AWS Spot Instanceを使ったAuto Scalingについてお話します。

サービスが抱えているGitLab CIでの問題

現在のRunnerとJobの設定

まず、経緯をお話する前に弊社のRunner状況について説明します。

弊社のGitLab Runnerは2種類用意されています。

1つ目は、GitLabを運用しているチームが用意したShared Runnerです。

こちらのRunnerはDocker Executorで動いており、GitLab CIにDocker Imageを指定すればサービス独自のコンテナ上でテストをすることが出来ます。また、全てのリポジトリからこのRunnerを使用することが出来ます。

image: node:10-alpine
stages:
  - build
job1:
  stage: build
  script:
    - node --version

2つ目は、データセンターのVMを使用したRunnerです。つまり、オンプレサーバーを使います。

このRunnerはサーバー上にSSH Executorで動いております。こちらはサービス独自のRunnerで、私たちのチームしか使えないRunnerになります。

こちらにはDockerのようにImage等を使用することが出来ませんが、インストールしたソフトウェアやライブラリは消えずに残り続けるのでキャッシュを使わなくても早い処理が見込めます。

最後にサービスのGitLabJobとGitブランチのRunnerは以下のような状況になっていました。

  • 本番ブランチ: テストJob、ビルドJob、デプロイJob
    • 全Job、オンプレサーバーのRunner
  • STGブランチ: テストJob、ビルドJob、デプロイJob
    • 全Job、オンプレサーバーのRunner
  • 開発ブランチ: ビルドJob、テストJob1(並列)、テストJob2(並列)
    • Shared Runner

何が問題なのか

上記のような状況で、開発ブランチのJobを何度か動かしているうちに以下のような問題が発生しました。

  • Shared Runnerは他のチームでも使用しているため、他のチームが大量にJobをリクエストするとこちらのJobが実行されるまで待つことがあった
  • Shared RunnerはJobを動かすスペックが高くないため、ジョブが完了するまで時間がかかってしまう
  • 開発ブランチは複数人が同時別ブランチをPushすることがあるため、テストJobが6つ同時に動くこともある。その場合、Shared Runner自体のメモリが足りずにエラーになってしまうということがあった

また、開発ブランチによるJob実行数は不安定であり急に増えることもあれば1日ずっとJobがこないこともあります。

こういったJobに対してShared Runnerに頼り続けるわけにもいかず、かといって専用のオンプレサーバーを立てるとたまにしか使わないサーバーを専有してしまうことになってしまいます。

Docker MachineとEC2(Spot Instance)を使ったRunner

GitLab Runnerではこのように一時的にJobが多くなるというパターンに対してAuto ScalingするRunnerであるDocker Machine Executorを用意しています。

https://docs.gitlab.com/runner/executors/docker_machine.html

Docker MachineとEC2の構成については日本語の紹介しているM3さんのテックブロク記事に詳しく書かれています。

https://www.m3tech.blog/entry/advent-calendar-2018-2
www.m3tech.blog

これによりJobが呼ばれたタイミングでEC2インスタンスが起動し、一定時間※1 経過するとインスタンスが削除されます。もちろん、削除される前に次のJobが実行されればインスタンスは削除されずに残り続けます。

EC2コストを下げるためにSpot Instanceを使用する

Jobが呼ばれたタイミングでしか課金されませんが、それでもc5.largeであれば1時間に$0.085かかります。

今回のような一時的にインスタンスを使用する際には、Spot Instanceを使うことによってコストを最大4分の1まで下げることが出来ます。

Spot Instanceを使うための設定も簡単でRunnerの設定 MachineOptions amazonec2-request-spot-instance=True を追加して、Spot Instanceに支払い最大値 amazonec2-spot-price=0.02 を追加するだけです。

設定値の全体的なコードは公式の資料が参考になります。

https://docs.gitlab.com/runner/configuration/runner_autoscale_aws/#getting-it-all-together

concurrent = 10
check_interval = 0

[[runners]]
  name = "gitlab-aws-autoscaler"
  url = "<URL of your GitLab instance>"
  token = "<Runner's token>"
  executor = "docker+machine"
  limit = 20
  [runners.docker]
    image = "alpine"
    privileged = true
    disable_cache = true
  [runners.cache]
    Type = "s3"
    Shared = true
    [runners.cache.s3]
      ServerAddress = "s3.amazonaws.com"
      AccessKey = "<your AWS Access Key ID>"
      SecretKey = "<your AWS Secret Access Key>"
      BucketName = "<the bucket where your cache should be kept>"
      BucketLocation = "us-east-1"
  [runners.machine]
    IdleCount = 1
    IdleTime = 1800
    MaxBuilds = 100
    OffPeakPeriods = [
      "* * 0-9,18-23 * * mon-fri *",
      "* * * * * sat,sun *"
    ]
    OffPeakIdleCount = 0
    OffPeakIdleTime = 1200
    MachineDriver = "amazonec2"
    MachineName = "gitlab-docker-machine-%s"
    MachineOptions = [
      "amazonec2-access-key=XXXX",
      "amazonec2-secret-key=XXXX",
      "amazonec2-region=us-central-1",
      "amazonec2-vpc-id=vpc-xxxxx",
      "amazonec2-subnet-id=subnet-xxxxx",
      "amazonec2-use-private-address=true",
      "amazonec2-tags=runner-manager-name,gitlab-aws-autoscaler,gitlab,true,gitlab-runner-autoscale,true",
      "amazonec2-security-group=docker-machine-scaler",
      "amazonec2-instance-type=m4.2xlarge",
    ]

※1 この時間はGitLab Runner設定のmachine IdleTime で定義出来ます。 https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-runnersmachine-section

改善結果

  • 改善前
    • 1vCPU、2GB
    • pipeline実行時間 16分
  • 改善後
    • 2vCPU、4GB(t3.medium)
    • pipeline実行時間 6分

今回の改善によって実行時間を10分間短縮することが出来ました。

どのくらいのスペックが良いかを調査

ただ単にスペックの高いインスタンスを用意するのも良いですが、テストJobがスペックを全て使いきれるとも限らないため、いろいろなインスタンスを用意して一番良い環境がどのなのかを調査しましたが、スペックを上げていっても結果は変わりませんでした。

t3.medium(2vCPU,4GB)→ 6分

c5.large(2vCPU,4GB)→ 6分

c5.xlarge(4vCPU,8GB)→ 6分

r5.large(2vCPU,16GB)→ 6分

Jobの実行時間が変わらなかった要因として考えられるのは、テスト自体が直列実行になっており、cpuとメモリを全て活用し切れていないのが要因なのではないかと思います。

次のアクション

今回の改善によって、GitLab CIの実行速度が上がり開発をより早く回せるようになりました。

次に行うべきアクションは以下のようなものが考えられます。

  • テストを並列で動かせるようにする
  • STGブランチと本番ブランチも今回作成したRunnerに移行する

これらのアクションをすぐに実行したいですが、現状でも大きく改善出来ておりまた上記の改善はそこまで大きい改善が見込めないため、さらにJobの時間がかかるようになったりした場合に再度改善に取り組みたいと思います。