アイリッジ開発者ブログ

アイリッジに所属するエンジニアが技術情報を発信していきます。

CDK で GitLab Shared Runner 作ってみた

f:id:iridge-tech:20210421131038p:plain 開発部の佐藤です。

弊社では複数の受託開発プロジェクトをまわしており、それぞれのチーム同士のコミュニケーションが不足しがちでした。その結果として、社内の共用GitLabの運用にあたって以下の課題を抱えていました。

  • 全社共有のGitLab Runner(以下Runner)と受託開発案件個別のGitLab Group Runner(以下Group Runner)がそれぞれ存在する
  • Runnerを保守をする主体が宙に浮いている
  • Group Runnerを構築する担当者の作り方によっては遊んでしまうリソースが多い
  • 受託開発案件ごとにGroup Runnerを構築していて保守に手間がかかる
  • Runnerの登録削除を考えておらず、Runnerがオフラインのままのゴミが溜まる
f:id:iridge-tech:20210421131107p:plain
確認した時には500を超えるrunnerが…

これらは本質的にはチーム・部署間のコミュニケーションに起因する課題です。この課題を解決するべくDevelopment Crossoverプロジェクトの光の戦士たちが立ち上がりました。

Development Crossoverプロジェクトとは

社内の有志で「プロジェクト横断でノウハウを共有し、属人化を避ける取り組みを行う」というゴールを掲げて活動しているプロジェクトです。参加メンバーは主にエンジニアなので、開発という選択肢をとることも多く、開発手法にはスクラムを採用し、1週間のイテレーションでベロシティを測る仕組みをまわしています。また プロジェクト横断で という目標の通り、どの部署のメンバーでも参加可能で、私の所属する部署以外のメンバーも参加しています。

これまでにも様々な活動を行っており、その中の一つであるOpenAPIモックサーバ用テンプレートの作成は技術者ブログで紹介しています。

https://iridge-tech.hatenablog.com/entry/2020/10/26/100000

Runnerのカイゼン

今回のクエストは、乱立するRunnerをいいかんじに集約して管理できるようにするのが目的です。解決策として以下の要件で共有ランナーの作成を行いました。

  • オフライン時に自動で登録解除される
  • 必要に応じてオートスケールする
  • 共用ランナーだけどパフォーマンスを犠牲にしない(主にAndroidのビルド対策)
    • S3キャッシュを利用する
    • EC2のインスタンスストアを利用する

またそれと同時に既存の共有ランナーの整理と今後のランナー管理体制の検討も行いました。

使用したツール類

  • CDK 1.94
  • TypeScript 4.2
  • projen 0.17

構成図

f:id:iridge-tech:20210421131142p:plain

頑張りポイント

TypeScriptの採用

  • TypeScript初挑戦のメンバーが多かったのですが、テストライブラリが充実していることからTypeScriptを採用しました。またそもそもCDK初挑戦のメンバーも複数いて、ペアプロ等を通してスキルアップを図りました

projenの使用

  • cdkは現在バージョンアップが頻繁で、都度バージョンを手作業で上げていくのはストレスが貯まります。projenを使用すればcdkのパッケージ更新が楽になり、さらに各種設定ファイルがprojen経由で生成できるので、管理が簡単に行えるようになります

コスト削減

  • spotfleetの使用
    • autoscaling.AutoScalingGroup でSpotPriceを指定するだけ!(cfnで実現しようとすると非常に面倒くさいです)

土日の縮退

  • オートスケーリンググループのcronによるスケール
    • autoscaling.AutoScalingGroupscaleOnScheduledesiredCapacityを1に更新
    • ターミネートのタイミングでGitLabランナーの登録を削除
// asg == AutoScalingGroup
// ライフサイクルフックに紐付けるlambda.Functionの設定
const onTerminatingHook = new lambda.Function(this, 'onTerminatingHook', {
        functionName: 'RunnerOnTerminatingLambdaFunction',
        code: lambda.Code.fromAsset(path.join(__dirname, '../assets/unregister')),  // コードを記載したファイルを指定
        handler: 'unregister.on_terminating',
        runtime: lambda.Runtime.PYTHON_3_8,
        role: ssmAutomatorRole,
    });

// ライフサイクルフックの設定
this.asg.addLifecycleHook('onTerminating', {
        defaultResult: autoscaling.DefaultResult.CONTINUE,
        heartbeatTimeout: cdk.Duration.minutes(1),
        lifecycleTransition: autoscaling.LifecycleTransition.INSTANCE_TERMINATING,
        notificationTarget: new hooktargets.FunctionHook(onTerminatingHook),  // ここでonTerminatingHookを指定している
    });
# assets/unregister.py
# codeを一部抜粋
def on_terminating(event, context):
    for record in event["Records"]:
        instance_id = json.loads(record["Sns"]["Message"])["EC2InstanceId"]
        unregister(instance_id)

def unregister(instance_id):
    # boto3のssmを使用
    ssm.send_command(
        InstanceIds=[instance_id],
        DocumentName="AWS-RunShellScript",
        Parameters={
            "commands": [
                "docker exec gitlab-runner gitlab-runner unregister --all-runners && kill -SIGQUIT $(pgrep gitlab-runner)"
            ],
            "executionTimeout": ["60"],
        },
    )

パフォーマンス改善

  • インスタンスストアの使用
// userDataにコマンドを追加
'mkdir /var/lib/docker',
'echo "/dev/nvme1n1 /var/lib/docker xfs defaults,nofail,noatime 0 2" >> /etc/fstab',
'mkfs -t xfs /dev/nvme1n1',
'mount -a',

EBSの掃除

  • 容量が逼迫するのを防ぐため1日1回dockerの未使用オブジェクトを削除する
// userDataにコマンドを追加
echo "0 0 * * * /usr/bin/docker container prune -f; /usr/bin/docker volume prune -f" > cron.conf && crontab cron.conf

監視

  • メトリクス監視
    • CloudWatch Agentを使用
    • 異常が検出されたらインスタンスをUnhealthyにしてSlack通知

宣伝

アイリッジではアットホームなやりがいのある開発案件にJOINしてくれる仲間を募集中です 😄 弊社の開発案件は大手のお客様が多く、自分が作ったモノが誰でも知っている企業様のアプリとして多くのユーザー様に使われる、という体験ができます。

CDKをはじめ、新しいツールをエンジニアが主導して導入できる職場です。コロナ禍の現在はエンジニア全員フルリモートで働いていますが、東京タワーを望むモダンで落ち着いたオフィスもあります。技術が好きなメンバーは本活動などを通して技術研鑽に励んでいます。