GeekFactory

int128.hatenablog.com

Terraform Moduleを用いたAWSにおけるGitLab Runnerの運用

GitLabにはCI/CDの機能が統合されています.GitLab CI/CDではGitLab本体とは別のGitLab Runnerと呼ばれるノードでビルドを実行します.GitLab RunnerはJenkinsでいうJenkins Agentと同様に,普通のPCで実行したり,クラウドでDockerコンテナとして実行したりできます.ドキュメントによるとGitLab Runnerの実行方法は以下が用意されています.

https://docs.gitlab.com/runner/#selecting-the-executor

Jenkins Agentと同様にGitLab Runnerは手軽にインスタンスを構築できる反面,スケールアウトやアップデートなどを考えておかないと運用が泥沼化しがちです.

本項では,GitHubで公開されているTerraform Moduleを利用してAWSでGitLab Runnerを運用する方法を紹介します.このモジュールを利用すると以下のメリットがあります.

  • AWSコンソールやSSHによる手動構築が不要になる
  • EC2インスタンスのメンテナンスが不要になる(Immutableな構成になっている)
  • ビルドキューに応じて実行ノードをスケールアウトできる

ただし,GitLabが公式にサポートしているモジュールではないので,今後も継続的にメンテナンスされていくかは分かりません.OSSなので積極的にcontributeしていきましょう.

terraform-aws-gitlab-runner

https://github.com/npalm/terraform-aws-gitlab-runner はGitLab Runnerに必要なリソースをプロビジョニングしてくれるモジュールです.具体的には以下のリソースが作成されます.

  • Auto Scaling Group(GitLab Runnerの実行用)
  • Launch Configuration(GitLab Runnerの実行用.GitLab Runnerのインストールスクリプトや設定ファイルが含まれる)
  • S3 Bucket(ビルドキャッシュ用)
  • CloudWatch Logs(EC2インスタンスのログ)
  • 上記で必要なSecurity GroupやIAM Roleなど

このモジュールはGitLab Runnerのauto scaling modeを利用しており,ビルドキューに応じてBuild RunnerのEC2インスタンスを作成および削除する仕組みになっています.具体的には,以下の2種類のEC2インスタンスが作成されます.

  • GitLab Runner(Build Runnerを作成するためのノード.Auto Scaling Groupがライフサイクルを管理する)
  • Build Runner(実際にビルドを実行するノード.GitLab Runnerがライフサイクルを管理する)

GitLab Runnerのauto scaling modeは内部でDocker Machineを利用しており,Docker Machineが新しいBuild Runnerを作成します.

このモジュールはGitLabオフィシャルの Autoscale GitLab CI/CD runners and save 90% on EC2 costs で紹介されている構成をベースにしているとのことですが,このモジュールではAuto Scaling Groupを利用してGitLab Runner自体もスポットインスタンスにすることでさらなる費用削減を実現しています.例えば,us-west-2リージョンで以下のインスタンスタイプを利用した場合,8〜9割引になります.(私の大好きな激安インスタンスタイプです)

  • GitLab Runner t3.micro $0.0104/h → $0.0031/h (80% OFF)
  • Build Runner m3.medium $0.0670/h → $0.0067/h (90% OFF)

今のところSpot Fleetには対応していないようですが,今後に期待です.(詳しくはIssue#77を参照)

使ってみよう

https://github.com/npalm/terraform-aws-gitlab-runner/tree/develop/examples にサンプルが用意されています.本稿では,パブリックサブネットにGitLab Runnerを配置する例を示します.

AWSリソースの定義

まず,新しいVPCとサブネットを定義します.IPアドレスブロックや名前は適当で構いません.

resource "aws_vpc" "main" {
  cidr_block = "172.21.0.0/16"
}

resource "aws_internet_gateway" "public" {
  vpc_id = "${aws_vpc.main.id}"
}

resource "aws_route_table" "public" {
  vpc_id = "${aws_vpc.main.id}"
}

resource "aws_route_table_association" "public" {
  route_table_id = "${aws_route_table.public.id}"
  subnet_id      = "${aws_subnet.public.id}"
}

resource "aws_route" "public_internet_gateway" {
  route_table_id         = "${aws_route_table.public.id}"
  destination_cidr_block = "0.0.0.0/0"
  gateway_id             = "${aws_internet_gateway.public.id}"
}

resource "aws_subnet" "public" {
  vpc_id            = "${aws_vpc.main.id}"
  cidr_block        = "172.21.0.0/20"
  availability_zone = "us-west-2a"
}

次に,terraform-aws-gitlab-runnerモジュールを読み込みます.ここでは https://gitlab.com にGitLab Runnerを登録しますが,独自にホスティングしているGitLabに対しても同様の方法で登録できます.

data "local_file" "public_ssh_key" {
  filename = ".sshkey.pub"
}

variable "gitlab_runner_registration_token" {
  type        = "string"
  description = "Registration token for GitLab Runners"
}

module "gitlab_runner" {
  source  = "npalm/gitlab-runner/aws"
  version = "3.5.0"

  aws_region                  = "us-west-2"
  environment                 = "gitlab_runner"
  ssh_public_key              = "${data.local_file.public_ssh_key.content}"
  vpc_id                      = "${aws_vpc.main.id}"
  subnet_ids_gitlab_runner    = ["${aws_subnet.public.id}"]
  subnet_id_runners           = "${aws_subnet.public.id}"
  aws_zone                    = "a"
  runners_use_private_address = false

  # GitLab Runnerインスタンスの設定
  instance_type              = "t3.micro"
  runner_instance_spot_price = "0.01"

  # Build Runnerインスタンスの設定
  docker_machine_instance_type  = "m3.medium"
  docker_machine_spot_price_bid = "0.02"

  runners_name                = "aws.m3"
  runners_gitlab_url          = "https://gitlab.com"

  # GitLab本体にGitLab Runnerを登録する際の設定
  # https://docs.gitlab.com/ee/api/runners.html#register-a-new-runner を参照
  gitlab_runner_registration_config = {
    registration_token = "${var.gitlab_runner_registration_token}"
    description        = "GitLab Runner on AWS"
    tag_list           = "aws"  # GitLab Runnerのタグ
    locked_to_project  = "true"
    run_untagged       = "false"
    maximum_timeout    = "3600"
  }
}

AWSリソースのプロビジョニングとGitLabへの登録

https://gitlab.com を開き,新しいプロジェクトを作成します.プロジェクトのCI/CD設定を開き,Registration Tokenを取得します.

f:id:int128:20190620174128p:plain

Terraformを実行します.ここでは環境変数でRegistration Tokenを指定します.

export TF_VAR_gitlab_runner_registration_token=dvc3717mx7RksPZCbe7a
terraform init
terraform apply

GitLab Runnerの登録に成功すると,以下のようにプロジェクトのCI/CD設定にRunnerが現れます.

f:id:int128:20190620174708p:plain

ビルドの実行

プロジェクトに .gitlab-ci.yml を作成します.このとき以下のように tags にGitLab Runnerのタグ名を指定します.先ほどTerraformの定義で aws というタグを付けたので,ここでも aws というタグを指定しています.

image: golang:1.12
stages:
  - test
format:
  stage: test
  script:
    - go test -race ./...
  tags:
    - aws

コミットをpushするとビルドが実行されます.AWSコンソールを確認すると,以下のようにBuild Runnerのインスタンスが作成されているはずです.

f:id:int128:20190620175729p:plain

Build Runnerライフサイクルの変更

デフォルトでは,ビルドが完了してから10分間が経過するとBuild Runnerのインスタンスが削除されます.また,Build Runnerのインスタンス数の上限は設定されていません.

ここでは,ビルドが完了してから1分後にインスタンスが削除されるように変更します.また,インスタンス数の上限も2つに変更します.

module "gitlab_runner" {
  # 以下を追記
  runners_idle_time = 60
  runners_limit     = 2
}

AWSリソースの変更を反映します.

terraform apply

上記のコマンドを実行するとLaunch Configurationが変更されます.すでにGitLab Runnerが起動している場合は,既存のインスタンスを削除すると新しい設定でインスタンスが起動されます.

プロジェクトを開き,連続してビルドを実行してみましょう.同時実行数が2つに制限されているはずです.

f:id:int128:20190620184604p:plain

また,1分間が経過するとBuild Runnerのインスタンスが削除されているはずです.

auto scaling modeの仕組みや設定は https://docs.gitlab.com/runner/configuration/autoscale.html で詳しく説明されているので,ぜひ参照してください.

まとめ

https://github.com/npalm/terraform-aws-gitlab-runner を利用すると,AWSでGitLab Runnerを簡単に運用できます.さらに,スポットインスタンスを利用すると大幅な費用削減を実現できます.

二回目の育休日記(生後8ヶ月)

娘3歳半、息子8ヶ月になりました。

int128.hatenablog.com

int128.hatenablog.com

近況

最近は以下のようなスケジュールで生活しています。

  • 7:00 起床
  • 8:40 保育園に送る
  • 9:00-11:30 パパの自由時間
  • 昼食
  • ママの自由時間
  • 15:40 保育園にお迎え
  • 21:30 就寝

3ヶ月の頃と比べると生活サイクルがだいぶ安定してきましたが、依然として夜は息子があまり寝ない問題があります。そのため、まだ娘&パパと息子&ママで別々の部屋で寝ています。ベビーベッドを卒業したら同じ部屋に移行する予定です。

息子は午前中はだいたい布団で寝ています。午後は抱っこ紐じゃないと寝ないので大変です(ママの場合は布団でも寝るみたい)。

食料品などは以下から調達しています。

西友ネットスーパーとパルシステムはZaimに対応しているので便利です。生活費の精算や支出計画を省力化できます。あと、パルシステムの牛乳は美味しいですね。

昼食は近くの弁当屋さん、もしくはインドカレー屋さんが多いです。ランチはパパとママで一緒にとることで、情報共有の時間を作るようにしています。また、ルンバは毎日(外出する方がセット)、水回り掃除は毎週水曜、食料品発注も毎週月/水曜に固定して分担しています。この辺はScrum Masterの経験が生きている気がします。

自由時間は近くのカフェやコワーキングスペースで作業しています。ちょっと費用がかかるのが悩みです。

www.facebook.com

娘はだいぶ落ち着いて手が掛からなくなってきました。3歳特有の難しさはありますが、一緒に小旅行に出かけたりできるようになったので楽しいです。だんだんと息子が動き出したのでこれから大変そうです。

保育園のママ友ネットワークにも随分と馴染んできました。3月末で引越しする子のお別れ会に顔を出したりもしました(パパは私しかいませんでしたが)。

今後の課題

計画では育休を4月下旬まで取得する予定でしたが、保育園の送り迎えを考慮して5月下旬までに延伸しました。1歳前であれば育児休業給付金も問題なく支給されるようです(まだ延伸分の振込を確認できていませんが)。

娘の保育園は自宅から少し離れているので、送り迎えは自転車が必須です。息子を自転車に乗せられるのは早くても6月になるので、5月下旬に復帰して当面はリモートワークもしくは休みにしようと考えています。

復帰後の生活サイクルがどうなるか分からない不安はあります。徐々にフルタイムに戻れるように調整していくつもりです。

育休中の活動

拙作のkubectl pluginがKrewでインストールできるようになりました。あとはLayered Architectureに移行したり、雑なエラーハンドリングを見直したりといったリファクタリングを進めていました。

github.com

拙作のGradle PluginがGradle 5に対応しました。SSH PluginについてはCircleCI 1.0から2.0への移行をずっと放置していたのでとても大変でした。

github.com

github.com

あとはRelease Engineeringのツールを作ったりしていました。

github.com

github.com

github.com

まだ試行錯誤中ですが、Yahooの雨雲レーダーを補完するサービスも作っています。保育園の送り迎えの時に雨雲の隙間を狙って出たいというのがモチベーションです。天候はコロコロ変わるので難しいですね。

github.com

以上、育休8ヶ月目の近況でした。

GitHub APIでリポジトリにファイルをコミットするコマンドを作った

GitHub APIを利用してリポジトリにファイルをコミットするコマンド ghcp を作りました。シングルバイナリでgitコマンドに依存しないため、リリースなどでCIからリポジトリのファイルを書き換えたい場合に便利です。

github.com

使い方

GitHub ReleasesもしくはHomebrewからインストールできます。

# GitHub Releases
curl -L -o /usr/local/bin/ghcp https://github.com/int128/ghcp/releases/download/v1.3.0/ghcp_linux_amd64

# Homebrew
brew tap int128/ghcp
brew install ghcp

# Go
go get github.com/int128/ghcp

あらかじめGitHubの設定ページからトークンを取得して $GITHUB_TOKEN に設定しておく必要があります。

基本的な使い方は以下になります。

デフォルトブランチ(通常はmaster)にファイルをコミットする:

ghcp -u YOUR -r REPO -m MESSAGE files

指定したブランチにファイルをコミットする:

ghcp -u YOUR -r REPO -b BRANCH -m MESSAGE files

デフォルトブランチから新しいブランチを作成して、ファイルをコミットする:

ghcp -u YOUR -r REPO -B BRANCH -m MESSAGE files

指定した親ブランチから新しいブランチを作成して、ファイルをコミットする:

ghcp -u YOUR -r REPO -B BRANCH --parent PARENT -m MESSAGE files

応用例

GitHub Pagesへのリリース

CIでビルドしたファイルをGitHub Pagesにリリースする場合、これまでは git commitgit push を駆使したスクリプトを書く必要がありましたが、ghcpを使うと以下の1コマンドでリリースできます。

ghcp -u int128 -r sandbox -b gh-pages -m "Example commit" index.html

Homebrew tapリポジトリへのリリース

tapリポジトリにformulaをリリースする場合もghcpを使うと簡単に実現できます。

# formulaを生成する
cat > hello.rb <<EOF
class Hello < Formula
  desc "Your awesome application"
  homepage "https://github.com/YOUR/hello"
  url "https://github.com/YOUR/hello/releases/download/v1.0.0/hello_darwin_amd64"
  version "v1.0.0"
  sha256 "$(shasum -a 256 -b hello | cut -f1 -d' ')"

  def install
    bin.install "hello_darwin_amd64" => "hello"
  end

  test do
    system "#{bin}/hello -h"
  end
end
EOF

# tapリポジトリにコピー
ghcp -u int128 -r homebrew-sandbox -m v1.0.0 hello.rb

バージョン文字列を書き換える

READMEやビルドスクリプトなどのバージョン文字列を書き換える用途にも使えます。CIで新バージョンをテストする場合に便利ですね。

# substitute version string in files
sed -i -e "s/version '[0-9.]*'/version '$TAG'/g" README.md build.gradle

# commit the changes to a new branch
ghcp -u YOUR -r REPO -B bump-v1.1.0 -m v1.1.0 README.md build.gradle

まとめ

詳しい使い方は https://github.com/int128/ghcp を参照してください。