9
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

GitLabAdvent Calendar 2021

Day 13

GitLab-CI shared runner を用意してみんなに使ってもらおう!

Last updated at Posted at 2021-12-12

概要

社内で Git サーバを立てることになり、金銭面の都合やセキュリティ上の要求から、セルフホストする GitLab サーバを立てることがあると思います。
その場合、GitLab の利用者となる、社内の開発者は、GitLab に関係するサーバをどう管理するかいうことは考えずに開発に集中したいと考えます。 そうすると、自ずと、サーバ管理者がGitLab サーバに関係するインフラ面の管理を行い、開発者はサービスを利用するだけにするといった役割分担をしたくなります。

GitLab には GitLab-CI という機能があります。
これは GitHub Actions や CircleCI、CodeBuild などと同様に、リポジトリ内に yaml ファイルを定義しておくことで CICD を管理できる機能です。
この機能を利用すると、今までは Jenkins などで CI を回していたのを GitLab-CI に寄せていくことができます。
GitLab-CI に寄せることで、Jenkins という別の仕組みを知らなくて良くなるので、認知負荷を減らすことができます。また、リポジトリの内部と外部で責任範囲を明確にすることができるようになります。
もちろん Jenkins にも汎用性や複数のリポジトリをまたいだCICDを組み立てることができるメリットがありますが、影響範囲が一つのリポジトリ内に収まるのであれば、GitLab-CI のほうがメリットがあります。

セルフホストする GitLab-CI の利用を開始するには job を実行するためのインスタンスが必要です。
GitLab-CI として設定するインスタンスは3つの種類があります。

ここで、Specific Runner や Group runner を設定するには各リポジトリやグループの設定からする必要があるため、リポジトリやグループのOwner に当たる人ががインスタンスの設定を行う必要があります。そうすると、利用者側としてはコストを払いたくない、インスタンスの管理作業が発生してしまいます。

そこで、GitLab の管理者が Shared runner を用意することで、GitLab のサーバだけではなく GitLab-CI で利用するインフラに関しても、管理者と利用者で責任分界点を分けて、利用者はリポジトリの管理だけを行えば、良いというように役割を分けることができます。

手順

以下の手順で必要なコードは こちら にあります。

GitLab のリソースを立てる

shared runner を構築するために必要なリソースを立てます。
parameter の MyIp が 0.0.0.0/0 になっています。アクセス元が限定されるのであれば、セキュリティのため絞り込んでください。

cfn.yml
AWSTemplateFormatVersion: 2010-09-09
Description: "GitLab related resources"
Parameters:
  MyIp:
    Type: String
    Default: 0.0.0.0/0
  AvailabilityZone:
    Type: String
    Default: "ap-northeast-1a"
  SecurityGroupSuffix:
    Type: String
    Default: gitlabCiWorkerSecurityGroup
Resources:
  GitlabServer:
    Type: AWS::EC2::Instance
    Properties: 
      AvailabilityZone: !Ref AvailabilityZone
      EbsOptimized: true
      IamInstanceProfile: !Ref GitlabServerInstanceProfile
      ImageId: ami-036d0684fc96830ca
      InstanceType: m5a.large
      SecurityGroupIds: 
        - !Ref GitlabServerSecurityGroup
      SubnetId: !Ref PublicSubnet
      BlockDeviceMappings: 
      - DeviceName: "/dev/sda1"
        Ebs: 
          VolumeType: "gp3"
          DeleteOnTermination: "true"
          VolumeSize: "30"
      UserData:
        Fn::Base64: |
          #!/bin/bash
          apt-get update
          apt-get install ca-certificates curl gnupg lsb-release
          curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
          echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \
            $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
          apt-get update
          apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose
          docker -v
          docker-compose -v

          systemctl disable ssh
          systemctl stop ssh

          git clone https://github.com/athagi/gitlab-shared-runner-example.git
          cd gitlab-shared-runner-example
          PUBLIC_IP="$(curl 'http://169.254.169.254/latest/meta-data/public-ipv4')"
          ls -al
          sed -i -e "s/localhost/${PUBLIC_IP}/g" .env
          docker-compose up -d
  GitlabServerInstanceRole:
    Type: 'AWS::IAM::Role'
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - ec2.amazonaws.com
            Action:
              - 'sts:AssumeRole'
      Path: /
      ManagedPolicyArns:
        - "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
  GitlabServerInstanceProfile:
    Type: 'AWS::IAM::InstanceProfile'
    Properties:
      Path: /
      Roles:
       - !Ref GitlabServerInstanceRole
  GitlabServerSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: gitlab-ci-worker
      VpcId: !Ref VPC
      SecurityGroupIngress:
      - IpProtocol: tcp
        FromPort: 22
        ToPort: 22
        CidrIp: !Ref MyIp
      - IpProtocol: tcp
        FromPort: 80
        ToPort: 80
        CidrIp: !Ref MyIp
      - IpProtocol: icmp
        FromPort: -1
        ToPort: -1
        CidrIp: !Ref MyIp
  gitlabCiWorkerSecurityGroup: 
    Type: AWS::EC2::SecurityGroup
    Properties: 
      GroupDescription: gitlab-ci-worker
      VpcId: !Ref VPC
      GroupName: !Sub ${AWS::StackName}-${SecurityGroupSuffix}
      SecurityGroupIngress:
      - IpProtocol: tcp
        FromPort: 22
        ToPort: 22
        SourceSecurityGroupId: !Ref GitlabServerSecurityGroup
      - IpProtocol: tcp
        FromPort: 2375
        ToPort: 2375
        SourceSecurityGroupId: !Ref GitlabServerSecurityGroup
      - IpProtocol: tcp
        FromPort: 2376
        ToPort: 2376
        SourceSecurityGroupId: !Ref GitlabServerSecurityGroup
      - IpProtocol: icmp
        FromPort: -1
        ToPort: -1
        SourceSecurityGroupId: !Ref GitlabServerSecurityGroup
# this user is used by gitlab-ci config.toml
  gitlabCiUser:
    Type: AWS::IAM::User
    Properties: 
      Policies:
        - PolicyName: gitlab-ci-admin-policy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - 'ec2:*'
                Resource: '*'
  gitlabCiUserAccessKey:
    Type: AWS::IAM::AccessKey
    Properties:
      UserName: !Ref gitlabCiUser
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 192.168.0.0/16
  InternetGateway: 
    Type: AWS::EC2::InternetGateway
  InternetGatewayAttachment: 
    Type: AWS::EC2::VPCGatewayAttachment
    Properties: 
      InternetGatewayId: !Ref InternetGateway
      VpcId: !Ref VPC
  PublicSubnet: 
    Type: AWS::EC2::Subnet
    Properties: 
      AvailabilityZone: !Ref AvailabilityZone
      CidrBlock: 192.168.0.0/24
      MapPublicIpOnLaunch: true
      VpcId: !Ref VPC
  PublicRouteTable: 
    Type: AWS::EC2::RouteTable
    Properties: 
      VpcId: !Ref VPC
  PublicRouteA: 
    Type: AWS::EC2::Route
    Properties: 
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: "0.0.0.0/0"
      GatewayId: !Ref InternetGateway 
  PublicSubnetARouteTableAssociation: 
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties: 
      SubnetId: !Ref PublicSubnet
      RouteTableId: !Ref PublicRouteTable
Outputs:
  VPCID:
    Value: !Ref VPC
  SubnetID:
    Value: !Ref PublicSubnet
  Region:
    Value: !Ref "AWS::Region"
  AvailabilityZone:
    Value: !GetAtt PublicSubnet.AvailabilityZone
  Zone:
    Value: !Join ['', !Split ['ap-northeast-1', !GetAtt PublicSubnet.AvailabilityZone]] # Split delimiter cannot get !Ref value
  GitlabCiUserAccessKey:
    Value: !Ref gitlabCiUserAccessKey
  GitlabCiUserSecretAccessKey:
    Value: !GetAtt gitlabCiUserAccessKey.SecretAccessKey
  SecurityGroupName:
    Value: !Sub ${AWS::StackName}-${SecurityGroupSuffix}
  GitLabServerPublicIP:
    Value: !GetAtt GitlabServer.PublicIp
  GitLabServerInstanceID:
    Value: !Ref GitlabServer

deploy.sh
#!/bin/bash
STACK_NAME=gitlab-resources1
aws cloudformation deploy --template-file cfn.yaml --stack-name "${STACK_NAME}" --capabilities CAPABILITY_IAM

echo "== output variables =="
outputs=$(aws cloudformation describe-stacks --stack-name "${STACK_NAME}" | jq .Stacks[].Outputs)
echo "-- GitLab server info --"
echo "GitLabServerIP=$(echo "${outputs}" | jq '.[] | select(.OutputKey == "GitLabServerPublicIP")' | jq -r .OutputValue)"
echo "-- GitLab-CI [runners.machine] info --"
echo "amazonec2-access-key=$(echo "${outputs}" | jq '.[] | select(.OutputKey == "GitlabCiUserAccessKey")' | jq -r .OutputValue)"
echo "amazonec2-secret-key=$(echo "${outputs}" | jq '.[] | select(.OutputKey == "GitlabCiUserSecretAccessKey")' | jq -r .OutputValue)"

echo "amazonec2-region=$(echo "${outputs}" | jq '.[] | select(.OutputKey == "Region")' | jq -r .OutputValue)"
echo "amazonec2-vpc-id=$(echo "${outputs}" | jq '.[] | select(.OutputKey == "VPCID")' | jq -r .OutputValue)"
echo "amazonec2-subnet-id=$(echo "${outputs}" | jq '.[] | select(.OutputKey == "SubnetID")' | jq -r .OutputValue)"
echo "amazonec2-zone=$(echo "${outputs}" | jq '.[] | select(.OutputKey == "Zone")' | jq -r .OutputValue)"
echo "amazonec2-security-group=$(echo "${outputs}" | jq '.[] | select(.OutputKey == "SecurityGroupName")' | jq -r .OutputValue)"

ローカルで deploy.sh を実行すると GitLabサーバが立ち上がります。GitLabServerIP にアクセスするとページが開きます。
また、shared runner の設定に必要な値も出力されます。この値は後で利用します。

GitLab-CI shared runner の設定に必要なトークンを取得する

GitLab-CI を利用できるように設定を行っていきます。

GitLab の root ユーザのパスワードを取得するために、AWS コンソールにログインし、先程立てたインスタンスにセッションマネージャ経由で接続します。

image.png

ログインしたらGitLab のルートユーザのパスワードを取得します。

$ sudo docker exec -it gitlab-shared-runner-example_GitLabServer_1 grep 'Password:' /etc/gitlab/initial_root_password

取得したパスワードを利用して GitLab に root ユーザとしてログインします。
その後 {GitLabServerIP}/admin/runners に移動します。

image.png

Registration token を取得します。トークンは後で利用します。

GitLab-CI shared runner の設定を行う

GitLab-CI shared runner の設定をサーバ側で行っていきます。
今回は GitLab server と同居させますが、別のサーバで管理しても大丈夫です。

今回 gitlab runner では docker+machine executor を利用します。
これは、CI を実行する worker を gitlab runner が用意してそこで実行する方法です。
Docker machine は公式ではメンテナンスモードに移行されています1 が、GitLab ではそれをフォークして利用しています。

GitLab-CI の設定ファイルは config.toml です。
取得した GitLab-CI のトークンを有効化するために以下のコマンドを実行して、config.toml を取得します。
machine-option の値は、CloudFormation 作成時に取得した値で差し替えます。

config/config.toml.template
current = 4
check_interval = 0
log_level = "info"
[session_server]
  session_timeout = 1200
[[runners]]
  name = "gitlab-shared-runner"
  executor = "docker+machine"
  [runners.docker]
    tls_verify = false
    privileged = true
    disable_entrypoint_overwrite = false
    oom_kill_disable = false
    disable_cache = false
    shm_size = 0
  [runners.machine]
    IdleCount = 0
    IdleTime = 1200
    MachineDriver = "amazonec2"
    MachineName = "gitlab-runner-AS-%s"
    MaxBuilds = 10
    MachineOptions = [
      "amazonec2-access-key={{ amazonec2-access-key }}",
      "amazonec2-secret-key={{ amazonec2-secret-key }}",
      "amazonec2-region={{ amazonec2-region }}",
      "amazonec2-vpc-id={{ amazonec2-vpc-id }}",
      "amazonec2-subnet-id={{ amazonec2-subnet-id }}",
      "amazonec2-zone={{ amazonec2-zone }}",
      "amazonec2-use-private-address=true",
      "amazonec2-security-group={{ amazonec2-security-group }}",
      "amazonec2-instance-type=t3a.small",
      "amazonec2-root-size=16",
    ]

GitLab サーバで発行したトークンを有効化するために、以下のコマンドを実行します。
そうすると、トークンが有効化され、先程編集した config.toml.template を元に config.toml が生成されます。

docker run --rm -v $(pwd)/config:/etc/gitlab-runner gitlab/gitlab-runner register \
--non-interactive --executor docker+machine --url "http://{{ GitLabServerIP }}/" \
--registration-token "{{ GITLAB_RUNNER_TOKEN }}" \
--template-config /etc/gitlab-runner/config.toml.template \
--docker-image alpine

続いて、gitlab-ci のコンテナを立ち上げます。

docker-compose.yml
gitlab-ci:
  image: 'gitlab/gitlab-runner:latest'
  restart: always
  volumes:
    - './config:/etc/gitlab-runner'
    - '/var/run/docker.sock:/var/run/docker.sock'

コンテナを立ち上げると、GitLab の Admin エリアより、runner が登録されていることが確認できます。
image.png

ここまで来ると、

他のプロジェクトから確認してみる

適当なリポジトリを作成します。
{リポジトリ}/-/settings/ci_cd へアクセスし、 Runners のアコーディオンを開くと Shared runner が存在していることが確認できます。

image.png

このリポジトリのルートに .gitlab-ci.yml を作成します。

.gitlab-ci.yml

echo:
  stage: build
  image: busybox:latest
  script:
    - echo "hello world"

job が成功していることが確認できました!
image.png

図にすると以下のようになります。
arch-basic.png

Appendix

Docker build & push を行う

このままでは GitLab-CI のコンテナ内で完結する job は実行できるのですが、イメージをビルドすることはできません。
そこで gitlab runner を利用して Docker イメージの build と プライベートレジストリへの push を行えるようにします。
例として、docker build を行い、作成したイメージを Amazon ECR に push する仕組みを作ります。

gitlab-runner の設定

gitlab-runner の設定ファイルである config.toml に以下の設定を追加します。

config/config.toml
[[runners]]
  [runners.docker]
    privileged = true
    volumes = ["/var/run/docker.sock:/var/run/docker.sock", "/cache"]

このようにすることで、GitLab-CI が実行されるコンテナはホストのEC2の docker.sock を利用してビルドすることができます。

図にすると以下のようになります。

arch-build.png

リポジトリの設定

リポジトリに以下を追加します。

.gitlab-ci.yml
dockerBuild:
  stage: build
  image: public.ecr.aws/docker/library/docker:20.10.10
  before_script:
    - apk add --no-cache python3 py3-pip && pip3 install --upgrade pip && pip3 install awscli
    - aws --version
  script:
    - aws ecr get-login-password | docker login --username AWS --password-stdin https://${ECR_URL}
    - docker build -t ${ECR_URL}/myapp:${CI_COMMIT_SHORT_SHA} .
    - docker images
    - docker push ${ECR_URL}/myapp:${CI_COMMIT_SHORT_SHA}
  variables:
    ECR_URL: {{ myaccountid }}.dkr.ecr.ap-northeast-1.amazonaws.com
FROM public.ecr.aws/nginx/nginx:stable

RUN echo "hello world"

また、AWSリソースを操作するために必要なクレデンシャルを環境変数としてセットしておきます。

image.png

CI を実行してみると、、、

Inked68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f3235303231332f62656332343730302d383030652d356664332d326235652d3762323539343264323966302e7_LI.jpg

無事 push することができました!
image.png

一つの GitLab-CI の job に対して、一つのEC2 インスタンスが割り当てられ、同時に複数の job が同じインスタンスで実行されるということが docker+machine executor だとありません。docker executor を選択した場合は、同時に複数の job が実行される可能性があるのでここが大きな違いになります。実行中に他の悪意のある job によってイメージが削除されたり、隣で実行されている job がディスクを多く消費した結果 No space left on device となってしまうことを避けることができます。

また、config.toml[runners.machine] の MaxBuilds の値を設定することで、一つの EC2 インスタンスで実行できる job の最大数を設定することができます。
これを設定することで、一度利用したインスタンスは破棄して、job が実行されるたびにインスタンスを作り直したり、数回利用したら破棄するといったことができます。

private registry への透過的なアクセス

プライベートレジストリに push はできるようになりましたが、今度はプライベートレジストリのイメージを利用してビルドしたくなると思います。

サーバ側の設定

GitLab runner のインスタンスがログインせずに pull できるように、ecr-credential-helper の設定を行っていきます。
このときに注意が必要なのは、ログインを行う主体が gitlab runner(GitLab server に同居させているインスタンス)であって、gitlab runner によって作成されたインスタンスではないところです。

CFn テンプレートの GitlabServerInstanceRole.Properties.ManagedPolicyArns に対して、"arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly" を追加して再度デプロイします。

gitlab-runner を実行しているインスタンスで amazon-ecr-credential-helper の設定を行います。
また、gitlab-runner の docker-compose.yml にボリュームマウントの設定を追加します。

docker-compose.yml
gitlab-ci:
  image: 'gitlab/gitlab-runner:latest'
  restart: always
  volumes:
    - './config:/etc/gitlab-runner'
    - '/var/run/docker.sock:/var/run/docker.sock'
    - '/usr/bin/docker-credential-ecr-login:/usr/bin/docker-credential-ecr-login'

config.toml[[runners]] のセクションにも設定を追加します。

config/config.toml
[[runners]]
  environment = ["DOCKER_AUTH_CONFIG={\"credHelpers\":{\"{{ myaccountid }}.dkr.ecr.ap-northeast-1.amazonaws.com\":\"ecr-login\"}}"]

その後、gitlab-runner を再作成します。

リポジトリの設定

.gitlab-ci.yml に以下を追加します。

.gitlab-ci.yml
dockerPull:
  stage: build
  image: {{ myaccountid }}.dkr.ecr.ap-northeast-1.amazonaws.com/myapp:6aa3a135
  script:
    echo "docker pull is succeeded!"

実行してみると、、、

image.png

$DOCKER_AUTH_CONFIG の設定を元に、プライベートレジストリのイメージを利用して GitLab-CI が実行できました!

job の並列度を上げる

利用者が増えてくるとEC2 のオートスケーリングする最大数を増やしたくなってくると思います。
その時は config.toml の global セクションの concurrent の数を増やすこと2で解決できます。

job 実行までのオーバーヘッドを減らしたい

今回の説明では、初回 job を実行したタイミングで runner の EC2 インスタンスを作成してから job を実行していました。
しかし、インスタンスを用意するためのオーバーヘッドが掛かってしまい、CIの時間が長くなってしまいます。
そこで config.toml[runners.machine] セクションに IdleCount を追加3することで、job をいつでも実行できる状態になったインスタンスを待機させておくことができます。

夜間はスケールインさせたい

IdleCount を使うことで日中は快適にビルドすることができる環境ができました。
しかし、夜間利用者が少なくなる時間帯に、大量の待機状態のインスタンスを起動させておくことはもったいないです。

[[runners.machine.autoscaling]] を利用することで、特定の時間について AutoScaling の設定を上書きすることができます4
以下のような設定を config.toml に追加します。

[runners.machine]
  MachineName = "auto-scale-%s"
  MachineDriver = "amazonec2"
  IdleCount = 0
  IdleTime = 1200
  [[runners.machine.autoscaling]]
    Periods = ["* * 9-17 * * mon-fri *"]
    IdleCount = 10
    IdleTime = 3600
    Timezone = "Asia/Tokyo"

このようにすることで、営業時間中(平日の9-17時)では常に10台の待機状態のインスタンスを起動しておいて、それ以外の時間帯ではオンデマンドでインスタンスを用意することができるようになります。
IdleTime は、インスタンスをスケールインするまでの時間を設定できる値です。

雑感

GitLab のサーバの管理者をしていると、利用者にはコードを書くことだけに集中してもらいたいという考えが出てくると思います。
GitLab-CI の shared runner を整備することで、大半の CI を行いたい要求をカバーできるようになります。
利用者にとっても、わざわざ specific runner を用意する手間が省け、CI をすぐに導入できるメリットがあります。

Jenkins おじさんが社内の CI を支えるより、リポジトリを利用する開発者が CI をメンテしていくほうがライフサイクルを別に保つことができますし、なによりスケールすることができます。
実際に社内で shared runner を用意したところ、小回りの効くリポジトリを管理している人や yaml で管理できる CI ツールの利便性を知っている人は速い段階で自然と利用し始めてくれていました。
組織文化という違った問題も出てきますが、まずは shared runner を用意してみて、考えに同意してくれる仲間を集めてみてはいかがでしょうか。

  1. https://github.com/docker/machine/issues/4537#issue-341148889

  2. https://docs.gitlab.com/runner/configuration/runner_autoscale_aws/#the-global-section

  3. https://docs.gitlab.com/runner/configuration/autoscale.html#how-concurrent-limit-and-idlecount-generate-the-upper-limit-of-running-machines

  4. https://docs.gitlab.com/runner/configuration/autoscale.html#autoscaling-periods-configuration

9
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?