Gitlab Runner(docker) のAutoscale with AWS

Autoscaling GitLab Runner on AWS | GitLab

を試してみた話。

概要

Gitlab Runner は、実行方法としてdockerを選択すると、Gitlab Runner上でdocker runする形で実行されます。

その意味するところは、

  • Gitlab Runner本体のリソース上限の範囲でjobを実行しなければならない。
  • jobが1個でも2個でも消費するリソースは同一。
  • runnerが落ちると全てのjobが巻き込まれて落ちる。

というところです。

特に3つ目の点には注意が必要で、同時実行数の設定を間違うと、OOMキラーなどにより、gitlab runner プロセス自体が殺されたりします。つらい。

Gitlab Runner の docker+machine という実行方法を選択すると、Gitlab Runner自体ではjobが実行されず、docker machineを通して、他のノードで実行されるようになります。

そうすると

  • Gitlab Runner がdocker machineを通してアクセスできるノード1つにつき1jobが実行されるので死ぬのはjob単位。
  • Gitlab Runner 自体ではjobが実行されないので、Gitlab Runner自体が死ぬ可能性が低くなる
  • ノードの数がうまい具合に上下してくれるとjobの数だけしかリソースを消費しない。

というようなメリットが出てきます。 固定ノードによるdocker+machine 運用もありですが、3点目の利点を活かすには、オートスケールの仕組みを取り入れるのが良いです。 またノードにSpot Instanceを利用することで、コスト削減効果も見込めます。

といったところで本題の AWS を利用したAutoscaleです。

AWSを利用したAutoscaleと言っても、Gitlab Runnerが特別なことを行なっているわけではなく、docker machineの awsec2 driverがそのあたりの制御を行なっています。

なお、現在、docker machineはメンテナンスフェーズに入っており、 Direct support for AWS autoscaling groups and spot instances (#3877) · Issues · GitLab.org / gitlab-runner · GitLab のようなissueが立っていたりします。

具体的に何が問題なのかというと、docker machineがAmazonLinuxに対応していないため、ノードとしてubuntuを利用する必要がある、という点です。

ざっくりとした構成

gitlab runnerはGitlabをpollingして、jobがあればspot instanceを立ち上げ、実行を依頼する。

見ての通り、gitlab runnerには EC2インスタンスを作成するという、強めの権限が必要になります。

また、実行ノードには、状況に応じて、各種サービスへのアクセス権が必要となります。

セキュリティ

gitlab runner

構成で軽く説明した通り、gitlab runner には強い権限を与える必要があるため、乗っ取られたりするリスクを最小限にする必要があります。

具体的には、セキュリティグループなどを利用して、

  • Gitlabへのアクセス
  • ノードへのアクセス
  • OSのセキュリティアップデートなどのアクセス
  • 管理者がgitlab runnerを制御するためのインバウンドアクセス

しかできないようにすると良いでしょう。

ノードを作成するのに必要な権限としては

      "ec2:DescribeSpotInstanceRequests"
      "ec2:CancelSpotInstanceRequests"
      "ec2:GetConsoleOutput"
      "ec2:RequestSpotInstances"
      "ec2:RunInstances"
      "ec2:StartInstances"
      "ec2:StopInstances"
      "ec2:TerminateInstances"
      "ec2:CreateTags"
      "ec2:DeleteTags"
      "ec2:DescribeInstances"
      "ec2:ImportKeyPair"
      "ec2:DeleteKeyPair"
      "ec2:DescribeKeyPairs"
      "ec2:DescribeRegions"
      "ec2:DescribeImages"
      "ec2:DescribeAvailabilityZones"
      "ec2:DescribeSecurityGroups"
      "ec2:DescribeSubnets"

このあたりです。

ノード

ノードには、環境にもよりますが、キャッシュを利用するためS3へアクセスできるようにしたり、ECRを利用していればそちらへのアクセス権、 その他、外部サービスなどへのアクセスが必要であれば、それらの権限が必要になります。 こちらも、それなりに強い権限になることが予想されますので、

  • 必要なサービスへのアクセス
  • gltlab runner からのインバウンドアクセス

のみに限るようにしておくと良いでしょう。

インストールと設定

まずはGitlab Runnerのインストールです。

debian系(ubuntuほか)であれば

curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh | sudo bash

sudo apt-get install gitlab-runner

RedhatLinux系(Ceotsほか)であれば

curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.rpm.sh | sudo bash

sudo yum install gitlab-runner
sudo /usr/share/gitlab-runner/post-install

あとは docker, docker machine もインストールしておきます。

docker machine のインストールは Releases · docker/machine · GitHub を参考に。

/usr/local/bin/docker-machine へインストールすることを推奨していますが、一部環境では サービスの実行時に見に行くPATHに/usr/local/bin/ が含まれていないことがあるため、/usr/bin/docker-machine へインストール(もしくはシンボリックリンクを作成)する必要があるかもしれません。

次にgitlab runnerのGitlabへの登録です。

通常の手順と大きく変わりはしませんが、executorとしてdocker+machineを選んでください。

最後に設定

設定は/etc/gitlab-runner/config.tomlを直接変更する方がわかりやすい気がしています。

オフィシャルサイトに記載のサンプルを足したものがこちら

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 = 10
    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-zone=x",
      "amazonec2-use-private-address=true",
      "amazonec2-tags=runner-manager-name,gitlab-aws-autoscaler,gitlab,true,gitlab-runner-autoscale,true",
      "amazonec2-security-group=xxxxx",
      "amazonec2-instance-type=m4.2xlarge",
    ]

global

グローバルな設定として重要なものにconcurrentがあります、これは、gitlab runnerで同時に実行できるjobの上限を決めるものです。 状況に合わせてチューニングしましょう。0は無限を意味しないので気をつけてください。

runners

つぎに[[runners]]設定です。ここには、gitlab runnerを登録した時の情報が含まれています。runnerは一つのrunnerにつき、複数設定できます。

具体的には、gitlab-runner register するたびに増えていきます。例えば、リソースを多く必要とするjobとそうでないjobがある場合に、使う設定を変更したい、というような場合、複数登録するというのはアリかもしれません、gitlab runnerを複数用意する方が素直なケースが多いですが。

runnerの設定では、limitというのがあります、これは、立ち上げ可能なノードの数を示しています。実行中ノードと待機中ノードの数を足したものの上限が設定されます。

runners.docker

runnerごとの設定に[runners.docker]があります。ここで重要なのはprivilegedです、jobでdocker in docker が必要な場合(docker buildしたい場合など)はtrueに設定しておく必要があります。

runners.cache

同じくrunnerごとの設定に[runners.cache]があります。Type=s3とすることで、s3をキャッシュとして利用できます、またshared=trueとすることで、ノードでキャッシュが共用できるようになります。基本的にはsharedtrueで良いでしょう。

アクセスキーやシークレットについては、適切なinstance profileが設定されていれば不要です。

runners.machine

同じくrunnerごとの設定に[runners.machine]があります。 ここで、ノードの設定を行います。

アクセスキーやシークレットについては、適切なinstance profileが設定されていれば不要です。

  • IdleCountというのが、待機中のノードの数です。待機中のノードの数がこの数より少なくなったら、ノードを立ち上げて待機します。
  • IdleTimeというのは、ジョブが終わったあと、待機する時間です、IdleCountよりたくさんのノードが待機中担っていた場合、この時間がすぎるとノードはシャットダウンされます。
  • MaxBuildsというのは、ノードがこの回数jobを実行するとシャットダウンするという数値です。ノードを定期的にクリーンアップするのに利用できます。
  • OffPeakPeriodsは、いわゆる、夜間や休日に関する設定です。この期間は、IdleCountIdleTimeの代わりにOffPeakIdleCountOffPeakIdleTimeが利用されます。
  • MachineDriverこれは今回の説明ではAWSを利用すると言っているのでawsec2です、利用できるのはdocker machimeがサポートしているものです。
  • MachineOptions ここが重要なところで、ノードとして作成するインスタンスに関する情報を記載します。設定はだいたい見ての通りなので大丈夫でしょう。amazonec2-amiで利用するAMI IDが指定できますので、カスタムイメージを利用する場合は設定すると良いでしょう。ただし、AmazonLinuxベースのものは利用できません。

考えるべきこと

globaなconcurrent、ノード単位の limit, IdleCount が重要な数値となります。

runnerが1つの場合、

  • concurrent=15, limit=20, IdleCount=5 な状況では、最大20台のノードが立ち上がります。最大同時実行数は15です。(5台はidle状態をキープしている)
  • concurrent=15, limit=15, IdleCount=5 な状況では、最大15台のノードが立ち上がります。最大同時実行数は15です。(最大実行時にはidle状態のノードがなくなる)
  • concurrent=15, limit=10, IdleCount=5 な状況では、最大10台のノードが立ち上がります。最大同時実行数は10です。(limitのほうがconcurrentより小さいので、設定した最大実行数に満たない数しか同時実行されない)

runnerが複数の場合、

  • concurrent=15, limit=20, IdleCount=5, limit=20, IdleCount=5 な状況では、最大25台のノードが立ち上がります。最大同時実行数は15です。(各runner5台はidle状態をキープしている)

  • concurrent=15, limit=10, IdleCount=5, limit=10, IdleCount=5 な状況では、最大20台のノードが立ち上がります。最大同時実行数は15です。(各runnerでidle状態をキープしている台数はジョブの実行数に依存します)

  • concurrent=15, limit=5, IdleCount=5, limit=5, IdleCount=5 な状況では、最大10台のノードが立ち上がります。最大同時実行数は10です。(runnerの登録の仕方によりますが、片方のrunnerに余裕があるのに、もう片方のrunnerのlimitにひっかかって、同時実行数に余裕があるけれど同時実行されないケースも出てきます。)

というところで、環境に応じた設定が必要になりますし、runnerを複数設定することは、若干の無駄を許容するという感じになりそうです。