CodeBuild-hosted GitHub Actions runner使ってみた②〜実践編〜

はじめに

DTダイナミクスでSREセクションのテックリードをしている霜鳥です。
この記事はCodeBuild-hosted GitHub Actions runner使ってみた①〜紹介編〜の続編です。
今回は実際にCodeBuildをGitHub Actions(以下GHA)のself-hosted runnerとして設定してGHAから利用する方法をTerraformのコードを交えながらご紹介します。

その他、霜鳥が書いた過去の記事はこちら。

実装

詳細は前回記事に載せましたので今回はいきなり本題から行きます。
CodeBuildはGitHubリポジトリごと、使うイメージごとに作成する必要があるのでモジュール化しておくのがオススメです。
なおこのTerraformプロジェクトでは以下のディレクトリ構成とし、backendやproviderなどは割愛します。

/
├ main.tf
└ modules
   └ self-hosted_runner
     ├ variables.tf
     └ main.tf

前提

OpenID Connect(以下OIDC)やCodeConnectionsの設定は別途完了しているものとします。
OIDCの確認はIAM > IDプロバイダの一覧に以下があればOKです。

  • プロバイダ:token.actions.githubusercontent.com
  • タイプ:OpenID Connect

詳細は以下の記事をご参照ください。
GitHub ActionsとAWSの連携におけるOIDC認証の活用 - Qiita

CodeStarConnectionsの確認はデベロッパー用ツール > 接続の一覧に以下があればOKです
デベロッパー用ツールはCodeCommitやCodeBuild、CodePipelineと同じページにあります。

  • プロバイダー:GitHub
  • ステータス:利用可能

余談ですが記事執筆時点でIDプロバイダは「プロバイダ」で接続は「プロバイダー」と表記揺れが存在するため本記事は準拠しています。
決して執筆者の表記揺れではありません笑

main.tf

まずはmodule呼び出しているmain.tf側の実装です。
このブロックを複数書いていくことで様々なリポジトリ、イメージに対応させていくことになります。
ユースケースによって変更したい箇所を中心に変数化しておきました。

# main.tf
module "msim_rs_buildmachine" {
  source = "./modules/self-hosted_runner"

  # Variables
  ## Basic settings
  ### このマシン名はGitHub側でも使うので外から見て分かりやすい名前が良い
  build_machine_name = "CodeBuildのマシン名"
  ### このビルドマシンを使いたいGitHubのリポジトリURL
  repository_url     = "https://github.com/<owner>/<repository>"

  ## BuildMachine Specs
  ### 以下3つは正しい組み合わせが存在するので直下のリンク①を参照
  ### runner_imageはカスタムイメージのECRリポジトリでも可!!
  runner_compute_type = "BUILD_GENERAL1_SMALL"
  runner_type         = "LINUX_CONTAINER"
  runner_image        = "aws/codebuild/amazonlinux2-x86_64-standard:5.0"
  ### runner imageをpullする際にはデフォルトだとCodeBuildの標準ロールが用いられる
  ### クロスアカウントでECRからpullしたい際などCodeBuildに設定したサービスロールを用いたい場合は以下を設定
  image_pull_credentials_type = "SERVICE_ROLE"
  ## Networks(※VPC内にCodeBuildを立てる場合)
  vpc_id     = <VPC_ID>
  subnet_ids = <SUBNET_IDS>     # List(string)を想定
  security_group_ids = <SG_IDS> # List(string)を想定
}

リンク①: ビルド環境のコンピューティングモードおよびタイプ

variables

main.tfのVariablesで設定しているものを宣言しているブロックになるので割愛します。

モジュール本体(modules/self-hosted_runner/main.tf)

本記事のメインディッシュです。

IAM

まずはCodeBuildに設定するIAMロールから。
今回は実際のユースケースを想定して必要最低限の権限に加えて以下の3要件に対応できるように設定しています。

  • ログをCloudWatch Logsへ出力すること
  • CodeBuildをVPC内に配置すること
  • ECRのカスタムイメージを利用できること
# modules/self-hosted_runner/main.tf
###################################
##              IAM              ##
###################################
# ロール本体
resource "aws_iam_role" "runner" {
  name               = var.build_machine_name
  assume_role_policy = data.aws_iam_policy_document.runner_assume.json
}
# 信頼関係
data "aws_iam_policy_document" "runner_assume" {
  statement {
    effect = "Allow"
    actions = [
      "sts:AssumeRole",
    ]
    principals {
      type = "Service"
      identifiers = [
        "codebuild.amazonaws.com"
      ]
    }
  }
}
resource "aws_iam_role_policy" "runner_inline" {
  name   = var.build_machine_name
  role   = aws_iam_role.runner.id
  policy = data.aws_iam_policy_document.runner_inline.json
}
data "aws_iam_policy_document" "runner_inline" {
  # GitHubとの接続に必要な権限
  statement {
    effect = "Allow"
    actions = [
      "codeconnections:GetConnectionToken",
      "codeconnections:GetConnection",
      "codestar-connections:UseConnection",
      "codestar-connections:ListConnections"
    ]
    resources = [
      "*"
    ]
  }
  statement {
    effect = "Allow"
    actions = [
      "codeBuild:StartBuild",
    ]
    resources = [
      "arn:aws:codebuild:ap-northeast-1:${data.aws_caller_identity.current.account_id}:project/${var.build_machine_name}"
    ]
  }
  # Option: ログを出力するのに必要な権限
  statement {
    effect = "Allow"
    actions = [
      "logs:CreateLogStream",
      "logs:PutLogEvents",
    ]
    resources = [
      aws_cloudwatch_log_group.runner.arn,
      "${aws_cloudwatch_log_group.runner.arn}:log-stream:*",
    ]
  }
  # Option: VPC内に配置する場合に必要な権限
  statement {
    effect = "Allow"
    actions = [
      "ec2:CreateNetworkInterface",
      "ec2:DescribeNetworkInterfaces",
      "ec2:DeleteNetworkInterface",
      "ec2:DescribeSubnets",
      "ec2:DescribeSecurityGroups",
      "ec2:DescribeVpcs"
    ]
    resources = ["*"]
  }
  # Option: ECRのカスタムイメージを使うのに必要な権限
  statement {
    effect = "Allow"
    actions = [
      "ecr:GetAuthorizationToken",
      "ecr:GetDownloadUrlForLayer",
      "ecr:BatchCheckLayerAvailability",
      "ecr:InitiateLayerUpload",
      "ecr:UploadLayerPart",
      "ecr:CompleteLayerUpload",
      "ecr:PutImage",
      "ecr:BatchGetImage"
    ]
    resources = ["*"]
  }
}

ビルドマシン

buildspec.ymlを含んでいないため非常にシンプルです。
CodeBuildはWebhookで動かされるためwebhookのリソースとCloudWatch Logsのロググループのリソースが含まれています。

# modules/self-hosted_runner/main.tf
###################################
##           CodeBuild           ##
###################################
resource "aws_codebuild_project" "runner" {
  depends_on = [
    aws_iam_role.runner,
    aws_iam_role_policy.runner_inline
  ]

  name         = var.build_machine_name
  service_role = aws_iam_role.runner.arn

  source {
    type     = "GITHUB"
    location = var.repository_url
  }
  # CodePipelineなどで前後を接続していないのでここはNO_ARTIFACTSになる
  artifacts {
    type = "NO_ARTIFACTS"
  }

  environment {
    compute_type                = var.runner_compute_type
    type                        = var.runner_type
    image                       = var.runner_image
    image_pull_credentials_type = var.image_pull_credentials_type
    privileged_mode             = true # 内部でDockerを動かす必要があるので特権モードが必要
  }
  # VPC内で動かす際の設定
  vpc_config {
    vpc_id             = var.vpc_id
    subnets            = var.subnet_ids
    security_group_ids = var.security_group_ids
  }
  # ログを出す際の設定
  logs_config {
    cloudwatch_logs {
      group_name = aws_cloudwatch_log_group.runner.name
    }
  }

  cache {
    type = "LOCAL"
    modes = [
      "LOCAL_CUSTOM_CACHE"
    ]
  }
}

resource "aws_codebuild_webhook" "runner" {
  project_name = aws_codebuild_project.runner.name
  build_type   = "BUILD"

  filter_group {
    filter {
      type    = "EVENT"
      pattern = "WORKFLOW_JOB_QUEUED"
    }
  }
}

###################################
##              log              ##
###################################
resource "aws_cloudwatch_log_group" "runner" {
  name              = var.build_machine_name
  retention_in_days = 7
}

設定の確認

terraform applyでリソース群を作成するとrepository_urlで指定したGitHubリポジトリ
Settings > Webhooksに以下の画像のようにCodeBuildのマシンが見えるようになります。
出ていない場合はOIDCやConnection、リソースの設定などを見直してみてください。
特にVPC内に作成する際にはインターネットに出られるサブネット/セキュリティグループである必要があります。

Webhooks

CodeBuild-hosted GitHub Actions runner

それでは最後に実際にCodeBuildをSelf-hosted runnerとしてGHAから動かしてみましょう!
使い方は簡単でいつものGHAの定義ファイル内のJobsでruns-onにて以下のように指定するだけです。
${{ github.run_id }}-${{ github.run_attempt }}はそのままでOK。

runs-on: codebuild-<CodeBuildのマシン名>-${{ github.run_id }}-${{ github.run_attempt }}

お試しで以下に.github/workflows/test.ymlというサンプルコードを置いておきます。

# .github/workflows/test.yml
name: test
on:
  workflow_dispatch:

jobs:
  build_and_push:
    runs-on: codebuild-testmachine-${{ github.run_id }}-${{ github.run_attempt }}
    steps:
      - name: hello, world!
        run: echo "hello, world!"

既知の不具合

TerraformでVPC内にCodeBuildを作成した際、以下のようにPROVISIONINGにて
VPC_CLIENT_ERROR: Unexpected EC2 error: error while getting DHCP options for VPC
というエラーの出ることがあります。

ProvisioningError

設定自体は正しいのに出ることがあり、これのトラブルシュートとしてはかなり黒魔術チックになってしまうのですが、
マネジメントコンソールにて対象のCodeBuildのプロジェクトを編集を一度開き、何も設定を変更せずにプロジェクトを更新するを押すことで直ります。
自分で書いていてもわけが分からないのですがAPI経由だと正しく設定できていないのかもしれません。

さいごに

DTダイナミクスのSREチームでは様々な新しいチャレンジを通して#時間戦略#顧客時間価値にコミットしています。
わたしたちミスミ、そしてDTダイナミクスは一緒にmeviyを通して世界の製造業を支える仲間を募集しています!
少しでも興味のある方はぜひカジュアル面談しましょう!

www.wantedly.com