GeekFactory

int128.hatenablog.com

Kubernetes上のGitLab Runnerでビルドキャッシュを利用する

GitLab CI/CDではビルドキャッシュがサポートされています.本稿では,KubernetesにデプロイしているGitLab Runnerでビルドキャッシュを利用する方法を紹介します.

ここでは以下を利用している前提とします.

  • AWS
  • Kubernetes 1.13 (EKS)
  • kube2iam
  • Terraform 0.12
  • Helmfile v0.80 or later

以下の流れでビルドキャッシュを構成していきます.

  1. ビルドキャッシュ用のS3バケットを作成する.
  2. GitLab RunnerのIAMロールを作成する.
  3. GitLab Runnerが生成するworkerのIAMロールを作成する.
  4. GitLab Runnerをデプロイする.
  5. ジョブを設定する.

GitLab RunnerとAWSリソースの構成図を以下に示します.

f:id:int128:20190925174057p:plain

1. ビルドキャッシュ用のS3バケットを作成する

GitLab Runnerはデフォルトではローカルにビルドキャッシュを保存しますが,GitLab Runnerを使い捨てにする場合はビルドキャッシュが消えてしまいます. S3バケットを利用すると複数のGitLab Runnerでビルドキャッシュを共有できます.

以下のようにTerraformでS3バケットを作成します.

# S3 bucket for cache of GitLab Runner
# https://gitlab.com/gitlab-org/gitlab-runner/blob/master/docs/configuration/autoscale.md#distributed-runners-caching
resource "aws_s3_bucket" "gitlab_runner_cache" {
  bucket = "${var.cluster_name}-gitlab-runner-cache"
  acl    = "private"
}

2. GitLab RunnerのIAMロールを作成する

GitLab Runnerはジョブの最初にビルドキャッシュを取得します.また,ジョブの最後にビルドキャッシュを保存します.そこで,ビルドキャッシュ用のS3バケットを読み書きできるIAMロールを作成し,GitLab Runnerにアタッチします.正確には,GitLab Runnerが生成するworkerではなく,GitLab Runnerの本体にIAMロールをアタッチする必要があります.

以下のようにTerraformでIAMロールとIAMポリシーを作成します.

# IAM role for GitLab Runner
resource "aws_iam_role" "gitlab_runner" {
  name               = "${var.cluster_name}-gitlab-runner"
  assume_role_policy = data.aws_iam_policy_document.kube2iam_assume_role.json
}

resource "aws_iam_role_policy" "gitlab_runner" {
  role   = aws_iam_role.gitlab_runner.name
  policy = <<EOF
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:PutObject",
                "s3:GetObject",
                "s3:ListBucket",
                "s3:DeleteObject"
            ],
            "Resource": [
                "${aws_s3_bucket.gitlab_runner_cache.arn}/*",
                "${aws_s3_bucket.gitlab_runner_cache.arn}"
            ]
        }
    ]
}
EOF
}

data "aws_iam_policy_document" "kube2iam_assume_role" {
  statement {
    actions = ["sts:AssumeRole"]
    principals {
      type        = "Service"
      identifiers = ["ec2.amazonaws.com"]
    }
    principals {
      type        = "AWS"
      identifiers = [module.eks.worker_iam_role_arn]
    }
  }
}

3. GitLab Runnerが生成するworkerのIAMロールを作成する

GitLab Runnerはジョブを実行する時に新しいPodを生成します.ジョブで指定したイメージやスクリプトはPod内で実行されます.

以下のようにTerraformでIAMロールを作成します.

# IAM role for building app on GitLab Runner
resource "aws_iam_role" "gitlab_runner_app" {
  name               = "${var.cluster_name}-gitlab-runner-app"
  assume_role_policy = data.aws_iam_policy_document.kube2iam_assume_role.json
}

ここでは特に権限を割り当てていませんが,将来的に権限が必要になった場合はこのIAMロールに権限を割り当てます. 例えば,ECRにイメージを保存する必要が出てきた場合は以下のIAMポリシーをアタッチします.

resource "aws_iam_role_policy_attachment" "gitlab_runner_app_ecr" {
  role       = aws_iam_role.gitlab_runner_app.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryPowerUser"
}

4. GitLab Runnerをデプロイする

GitLab RunnerにはHelm Chartが用意されています.

以下のようにHelmfileでChartをデプロイします.

releases:
  - name: gitlab-runner-app
    namespace: gitlab
    chart: gitlab/gitlab-runner
    values:
      - gitlabUrl: {{ .Environment.Values.gitlabURL }}
        runnerRegistrationToken: {{ .Environment.Values.gitlabRunnerRegistrationToken }}
        rbac:
          create: true
        podAnnotations:
          # IAM role for GitLab Runner manager pod
          iam.amazonaws.com/role: CLUSTER-gitlab-runner
        runners:
          tags: "app"
          cache:
            # Distributed runners caching
            # https://gitlab.com/gitlab-org/gitlab-runner/blob/master/docs/configuration/advanced-configuration.md#the-runnerscaches3-section
            cacheType: s3
            cacheShared: true
            s3ServerAddress: s3.amazonaws.com
            s3BucketName: {{ .Environment.Values.cacheBucketName }}
            s3BucketLocation: {{ .Environment.Values.cacheBucketRegion }}
          podAnnotations:
            # IAM role for building apps
            iam.amazonaws.com/role: CLUSTER-gitlab-runner-app

environments:
  default:
    values:
      - gitlabURL: https://gitlab.example.com
        gitlabRunnerRegistrationToken: YOUR_TOKEN
        cacheBucketName: CLUSTER-gitlab-runner-cache
        cacheBucketRegion: ap-northeast-1

ポイントは以下です.

  • podAnnotations でGitLab Runnerの本体に割り当てるIAMロールを指定します.
  • runners.podAnnotations でGitLab Runnerが生成するworkerに割り当てるIAMロールを指定します.
  • runners.cache.cacheType には s3 を指定します.これを忘れるとS3バケットが有効になりません.
  • runners.cache.cacheShared はtrueにします.これを忘れるとビルドキャッシュが別のGitLab Runnerに引き継がれません.

GitLab Runnerでビルドキャッシュを設定する方法は以下のドキュメントが参考になります.

5. ジョブを設定する

GitLabのリポジトリ.gitlab-ci.yml を作成します.WebIDEでファイルを作成すると,ビルドジョブの動作をすぐに確認できるので便利です.

build:
  cache:
    paths:
      - .terraform

ジョブのログに以下が出力されていればビルドキャッシュが有効になっています.

Checking cache for default...
Downloading cache.zip from https://NAME-gitlab-runner-cache.s3-ap-northeast-1.amazonaws.com/project/NUMBER/default 
Successfully extracted cache

...


Creating cache default...
.terraform: found 140 matching files
Downloading cache.zip from https://NAME-gitlab-runner-cache.s3-ap-northeast-1.amazonaws.com/project/NUMBER/default 
Created cache

トラブルシューティング

GitLab Runnerの本体がS3バケットにアクセスできない場合は,ジョブのログに以下が出力されます.

Checking cache for NAME...
No URL provided, cache will not be downloaded from shared cache server. Instead a local version of cache will be extracted. 

...

Creating cache NAME...
No URL provided, cache will be not uploaded to shared cache server. Cache will be stored only locally. 

また,GitLab RunnerのPodのログに以下が出力されます.

error while generating S3 pre-signed URL: No IAM roles attached to this EC2 service

Podに適切なIAMロールが割り当てられているか確認しましょう.GitLab Runnerが生成するworkerにS3アクセスのIAMポリシーを割り当てている場合もこれらログが出力されます.

GitLab CI/CDで特定のファイルが変更された場合にのみジョブを実行する

GitLab 11.4から only:changes という記法がサポートされました.これにより,特定のファイルが変更された場合のみジョブを実行できます.

https://docs.gitlab.com/ee/ci/yaml/#onlychangesexceptchanges

例えば,以下のように記述すると,ブランチをpushした時に terraform/ フォルダのファイルが変更されている場合にのみジョブが実行されます.

stages:
  - build

terraform_plan:
  stage: build
  only:
    changes:
      - terraform/**/*
      - .gitlab-ci.yml
  image:
    name: hashicorp/terraform:0.12.8
    entrypoint:
      - /usr/bin/env
  script:
    - cd terraform/
    - terraform --version
    - terraform init
    - terraform validate
    - terraform plan -out=plan.tfplan
  artifacts:
    name: plan
    paths:
      - terraform/plan.tfplan
  tags:
    - terraform

only はブランチ名と組み合わせることも可能です.以下のように記述すると,先ほどの条件に加えて,masterブランチに対してのみジョブが実行されます.

  only:
    changes:
      - terraform/**/*
      - .gitlab-ci.yml
    refs:
      - master

.gitlab-ci.yml を変更した場合は何らかの副作用がある場合が多いので,上記のように .gitlab-ci.yml を変更した場合もジョブが実行されるようにしておくとよいでしょう.

TerraformでIPアドレスリストをCIDRリストに変換する

Terraformの小ネタです.プライベートサブネットからInternet facing ALBへのアクセスを許可するため,NATゲートウェイグローバルIPアドレスをセキュリティグループに追加する必要がありました.Terraformの aws_security_group_rule にはIPアドレスではなくCIDRのリストを指定する必要があります.IPアドレスリストをCIDRリストに変換するにはどう書けばよいのか試行錯誤したのでメモを残しておきます.

Terraform v0.12を前提とします.

TL;DR

resource "aws_security_group_rule" "allow_from_natgw" {
  cidr_blocks = [for ip in ips : "${ip}/32"]
}

解説

terraform-aws-vpc モジュールを利用して,パブリック/プライベートサブネット構成を作成します.

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "2.9.0"

  enable_nat_gateway   = true
  # 中略
}

セキュリティグループを作成するには aws_security_group リソースを利用します.

resource "aws_security_group" "alb_foo" {
  name        = "alb-foo"
  vpc_id      = module.vpc.vpc_id
}

セキュリティグループにルールを追加するには aws_security_group_rule リソースを利用します.

resource "aws_security_group_rule" "alb_foo_from_natgw" {
  description       = "Allow from NAT Gateway"
  security_group_id = aws_security_group.alb_foo.id
  type              = "ingress"
  from_port         = 443
  to_port           = 443
  protocol          = "tcp"
  cidr_blocks       = [for ip in module.vpc.nat_public_ips : "${ip}/32"]
}

Terraform v0.12から for Expressions が利用できます.例えば,NATゲートウェイグローバルIPアドレス["1.2.3.4", "5.6.7.8"] の場合は以下のように展開されます.

  cidr_blocks = ["1.2.3.4/32", "5.6.7.8/32"]

もっと簡潔な書き方があったらぜひ教えてください!