TerraformでGitHub Actionsのセルフホストランナーとして動作するAWS CodeBuildを自動構築する

# はじめに
以前の記事では、GitLab Self Managedに接続するAWS CodeBuildとAWS CodePipelineを作成してみた。

https://qiita.com/neruneruo/items/b570fb6f739f1efb8250

https://qiita.com/neruneruo/items/1535776e5cab5df115bd

GitLab Self Managedとのインテグレーションでは、AWS CodeBuildやAWS CodePipelineの実行結果をプルリクエスト上で確認するためにはいろいろな作り込みが必要になり、それ自体がトイルになる可能性があった。

一方で、GitHubでは、2024/4/24より、GitHub ActionsのセルフホストランナーとしてAWS CodeBuildを指定できるようになった。

https://aws.amazon.com/jp/about-aws/whats-new/2024/04/aws-codebuild-managed-github-action-runners/

これにより、GitHub Actionsのプリミティブな機能を活用しつつ、コンピューティングリソースとしてAWS CodeBuildの環境を利用して、利用料金をAWSに集約したり、AWSリソースにアクセスしやすくすることが可能になった。

今回は、このGitHub Actionsとの連携をTerraformで作成していく。

# GitHubのパーソナルアクセストークンを払い出す
パーソナルアクセストークン未設定の場合は、以下のページを参考にしながら、GitHubのパーソナルアクセストークンを払い出しておく。

https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens

今回の検証に用いる機能のために、トークンには以下の権限を付与する。

|権限名|権限|必要な処理|
|:---|:---|:---|
|Administration|Read and write|リポジトリの作成|
|Contents|Read and write|ブランチの作成|
|Metadata|Read-only|デフォルトで付与(外せない)|
|Variables|Read and write|GitHub Actions変数の作成|
|Webhooks|Read and write|WebHookの作成|

# TerraformのProvider設定
上記で払い出したトークンを使ってアクセスするように、以下のようにProvdiderを設定する。

:::note
アクセストークンをコードに埋め込むのはアンチパターンであるため、今回はTerraformの変数として渡すようにする。
:::

```HCL
terraform {
  required_providers {
    github = {
      source = "integrations/github"
    }
  }
}

provider "github" {
  token = var.github_token
}
```

# AWS CodeBuildが使用するIAMロールの設定
IAMロールの設定は、最低限必要なのは以下のCodeBuildで標準的に必要になる設定のみだ。

```HCL
resource "aws_iam_role" "codebuild" {
  name               = local.iam_codebuild_role_name
  assume_role_policy = data.aws_iam_policy_document.codebuild_assume.json
}

data "aws_iam_policy_document" "codebuild_assume" {
  statement {
    effect = "Allow"

    actions = [
      "sts:AssumeRole",
    ]

    principals {
      type = "Service"
      identifiers = [
        "codebuild.amazonaws.com",
      ]
    }
  }
}

resource "aws_iam_role_policy" "codebuild" {
  name   = local.iam_codebuild_policy_name
  role   = aws_iam_role.codebuild.id
  policy = data.aws_iam_policy_document.codebuild_custom.json
}

data "aws_iam_policy_document" "codebuild_custom" {
  statement {
    effect = "Allow"

    actions = [
      "codeBuild:StartBuild",
    ]

    resources = [
      aws_codebuild_project.example.arn,
    ]
  }
  statement {
    effect = "Allow"

    actions = [
      "logs:CreateLogStream",
      "logs:PutLogEvents",
    ]

    resources = [
      aws_cloudwatch_log_group.codebuild.arn,
      "${aws_cloudwatch_log_group.codebuild.arn}:log-stream:*",
    ]
  }
}
```

# Amazon CloudWatch Logsの設定
Amazon CloudWatch Logsは今回はとりあえず設定しているだけなので、あまり細かいことは気にしていない。
実際に利用する際は組織のポリシーに合わせて設定を変更しよう。

```HCL
resource "aws_cloudwatch_log_group" "codebuild" {
  name              = local.clouddwatch_loggroup_name
  retention_in_days = 3
}
```

# GitHubの設定
## リポジトリ本体の設定
リポジトリは```integrations/github```プロバイダの```github_repository```のリソースで作成する。
特段難しいことはない。

ブランチの作成は、mainブランチに何かしらのソースコードが入っていないと行えないため、下記の作成後、適当なファイルをPUSHしておこう。

```HCL
resource "github_repository" "example" {
  name        = "example"
  description = "GitHubとCodeBuildのインテグレーションのサンプル"

  visibility = "private"
}
```

## ブランチの作成
ブランチの作成も特に難しいことはない。
```source_branch```は省略した場合は```main```が自動的に選択される。

```HCL
resource "github_branch" "example_feature_test1" {
  repository     = github_repository.example.name
  source_branch  = "main"
  branch         = "feature/test1"
}
```

## GitHub Actionsがプルリクエストを更新する権限を付与
設定はしなくても最悪メイン動作には影響がないが、GitHub Actionsが検査した結果等をプルリクエストのコメントとして書き戻すには必要な設定となる。

:::note info
この設定はまだTerraformに対応していない。プルリクエストは発行されているようなので、マージされるのを待とう。
:::

https://github.com/integrations/terraform-provider-github/pull/2309

```HCL
resource "terraform_data" "github_workflow_permission_change" {
  provisioner "local-exec" {
    command = <<-EOF
      curl -s -L \
      -X PUT \
      -H "Accept: application/vnd.github+json" \
      -H "Authorization: Bearer ${var.github_token}" \
      -H "X-GitHub-Api-Version: 2022-11-28" \
      https://api.github.com/repos/${github_repository.example.full_name}/actions/permissions/workflow \
      -d '{"default_workflow_permissions":"write","can_approve_pull_request_reviews":false}'
    EOF

    on_failure = fail
  }
}
```

# AWS CodeBuildの設定
## AWS CodeBuild本体
AWS CodeBuildは以下のように設定する。
AWS CodeBuild自体は特に凝った設定はない。今回、GitHub Actions内でMarketplaceからコンテナ動作するActionを引っ張ってくるため、```privileged_mode = "true"```で特権モードで動作するようにしておこう。

```HCL
resource "aws_codebuild_project" "example" {
  name         = local.codebuild_project_name
  service_role = aws_iam_role.codebuild.arn

  source {
    type     = "GITHUB"
    location = github_repository.example.http_clone_url
  }

  artifacts {
    type = "NO_ARTIFACTS"
  }

  environment {
    type            = "LINUX_CONTAINER"
    compute_type    = "BUILD_GENERAL1_SMALL"
    image           = "aws/codebuild/amazonlinux2-x86_64-standard:5.0"
    privileged_mode = "true"
  }

  logs_config {
    cloudwatch_logs {
      group_name = aws_cloudwatch_log_group.codebuild.name
    }
  }

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

## AWS CodeBuildのWebHook
AWS CodeBuildのWebHookでは、```WORKFLOW_JOB_QUEUED```のイベントパターンを設定する。
プルリクエストやPUSH時といったトリガは、GitHub Actions側で設定するため、こちらでは細かい設定は不要だ。

AWSがトークンを参照できるようにするために、```aws_codebuild_source_credential```で入力したトークンを渡そう。

```HCL
resource "aws_codebuild_source_credential" "example" {
  auth_type   = "PERSONAL_ACCESS_TOKEN"
  server_type = "GITHUB"
  token       = var.github_token
}

resource "aws_codebuild_webhook" "gitlab_example" {
  project_name = aws_codebuild_project.example.name
  build_type   = "BUILD_BATCH"

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

## GitHub Actionsに渡す変数の作成
後の処理を楽にするために、以下のGitHub Actions変数を作成しておく。

```HCL
resource "github_actions_variable" "example_variable" {
  repository    = github_repository.example.name
  variable_name = "CODEBUILD_RUNS_ON_NAME"
  value         = "codebuild-${aws_codebuild_project.example.name}"
}
```

# GitHub Actionsの設定
ここまで来たらやっとGitHub Actionsの設定だ。
リポジトリトップからのパスで```.github/workflows/main.yml```に以下のファイルを置こう。

今回、GitHub Actionsの設定内容についてがメインではないので詳細は割愛する。

なるべくこのYAMLファイルをテンプレート化して再利用性を高めた方が良いため、AWS CodeBuildのプロジェクト名を外部から注入できるようにした。これまでの準備で作成した、```CODEBUILD_RUNS_ON_NAME```を、```${{ vars.CODEBUILD_RUNS_ON_NAME }}```で参照することで、リポジトリを増やした場合もこの部分を変更することなく使い回せるようになる。

なお、```jobs.build```のブロックは、GitHub Actionsがデフォルトで用意するHello World的なものだ。
これだけだと味気ないので、HadolintをMarketplaceから入れて実行してみる。

https://github.com/marketplace/actions/hadolint-action

```jobs.hadolint.steps.name```が```Update Pull Request```のブロックは、上記リンクにも記載されている、プルリクエストに対する書き戻しの処理だ。
ただし、Hadolintは正常終了時に何も結果を出力しないため、ifのブロックに```&& steps.hadolint_execution.outcome == 'failure'```を追加して、エラー時のみコメントを書くように変更している。
また、これにより、エラーの場合も```jobs.hadolint```が正常終了してしまうため、最後に再度判定を入れて判定エラー時は```exit 1```をするようにしている。

```YAML:.github/workflows/main.yml
# This is a basic workflow to help you get started with Actions

name: CI-on-CodeBuild

# Controls when the workflow will run
on:
  # Triggers the workflow on push or pull request events but only for the "main" branch
  push:
    branches: [ "main" ]
  pull_request:
    branches: [ "main" ]

  # Allows you to run this workflow manually from the Actions tab
  workflow_dispatch:

# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
  # This workflow contains a single job called "build"
  build:
    # The type of runner that the job will run on
    runs-on: ${{ vars.CODEBUILD_RUNS_ON_NAME }}-${{ github.run_id }}-${{ github.run_attempt }}

    # Steps represent a sequence of tasks that will be executed as part of the job
    steps:
      # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
      - uses: actions/checkout@v4

      # Runs a single command using the runners shell
      - name: Run a one-line script
        run: echo Hello, world!

      # Runs a set of commands using the runners shell
      - name: Run a multi-line script
        run: |
          echo Add other actions to build,
          echo test, and deploy your project.
          ls -a -1
  hadolint:
    runs-on: ${{ vars.CODEBUILD_RUNS_ON_NAME }}-${{ github.run_id }}-${{ github.run_attempt }}
    steps:
      - uses: actions/checkout@v4
      - name: Hadolint Execution
        id: hadolint_execution
        uses: hadolint/hadolint-action@v3.1.0
        continue-on-error: true
      - name: Update Pull Request
        uses: actions/github-script@v6
        if: github.event_name == 'pull_request' && steps.hadolint_execution.outcome == 'failure'
        with:
          script: |
            const output = `
            #### Hadolint: \`${{ steps.hadolint.outcome }}\`
            \`\`\`
            ${process.env.HADOLINT_RESULTS}
            \`\`\`
            `;

            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: output
            });
      - name: Error exit actions
        if: steps.hadolint_execution.outcome == 'failure'
        run: exit 1
```

# いざ、動かす！
さて、ここまでやってみたら、プルリクエストを上げてみよう。
今回、Dockerfileの先頭を

```Dockerfile
FROM public.ecr.aws/nginx/nginx
```

のようにして、Hadolintがエラーになるようにしてみた。
実際に、GitHub Actionsのコンソール画面では、

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/a670687a-69de-7148-1c8f-e3f263d7a1d5.png)

といった感じコメントでエラー内容が表示されている。

これで、AWS CodeBuildを使ってお手軽にGitHub Actionsを動作させられるようになった！