AWS CodeBuildでTerraformを実行する

2023年8月31日

AWS CodeBuild

GitHub リポジトリにある Terrafrom のコードを AWS CodeBuild で実行する方法の備忘録です。

以下の状態を目指します。

  • GitHub のプルリクエストを作成したら、terraform plan コマンドを実行
  • GitHub のプルリクエストがマージされたら、terraform plan と apply コマンドを実行
  • CodeBuild プロジェクトも Terraform コードで管理する

動作確認をした Terraform のバージョンは 1.5.6、hashicorp/aws provider のバージョンは 5.14.0 です。

CodeBuild はとっつきにくいところもありますが、他の CI/CD サービスと比べて料金が安かったり、AWS リソースを安全に簡単に操作できたり良いところもあります。CodeBuild が適したケースもあると思いますので、選択肢としてご検討ください。

目次

  1. Terraform のコードの参照先
  2. AWS アカウント ID とリージョン
  3. IAM Policy
  4. IAM Role
  5. CodeBuild Project
  6. Webhook
  7. 初回は Terraform をローカル PC などで実行する
  8. 初回は Webhook のリソース作成に失敗する
  9. CodeBuild に保存する GitHub アカウントを決める
  10. CodeBuild から GitHub に接続する
  11. 再び Webhook のリソースを作成する
  12. buildspec.yaml
  13. buildspec.yaml は インラインか S3 に保存することも検討を
  14. Terraform のバージョンアップの際は 2 回プルリクエストの実行が必要

Terraform のコードの参照先

この後 Terraform のコードの解説をします。コードは以下の Terraform provider のドキュメントを参考に組み立てました。

AWS アカウント ID とリージョン

AWS アカウント ID とリージョンを Terraform コード内から参照できるようにします。

data "aws_region" "current" {}

data "aws_caller_identity" "current" {}

IAM Policy

上から順に、何のためのポリシーかを解説すると

  • Terraform から S3 の my-tfstate-bucket に tfstate ファイルにアクセスするための IAM Policy
  • AWS のマネジメントコンソールで CodeBuild プロジェクトを作成したときに自動で作成される IAM Policy
  • CodeBuild Project と Webhook を作成・変更・削除するための IAM Policy
resource "aws_iam_policy" "main" {
  name = "my-policy"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = [
          "s3:ListBucket",
        ]
        Effect   = "Allow"
        Resource = "arn:aws:s3:::my-tfstate-bucket"
      },
      {
        Action = [
          "s3:GetObject",
          "s3:PutObject",
        ]
        Effect   = "Allow"
        Resource = "arn:aws:s3:::my-tfstate-bucket/*"
      },
      {
        Action = [
          "logs:CreateLogGroup",
          "logs:CreateLogStream",
          "logs:PutLogEvents"
        ]
        Effect   = "Allow"
        Resource = [
          "arn:aws:logs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:log-group:/aws/codebuild/my-codebuild-project",
          "arn:aws:logs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:log-group:/aws/codebuild/my-codebuild-project:*"
        ],
      },
      {
        Action = [
          "s3:PutObject",
          "s3:GetObject",
          "s3:GetObjectVersion",
          "s3:GetBucketAcl",
          "s3:GetBucketLocation"
        ]
        Effect   = "Allow"
        Resource = [
          "arn:aws:s3:::codepipeline-${data.aws_region.current.name}-*"
        ],
      },
      {
        Action = [
          "codebuild:CreateReportGroup",
          "codebuild:CreateReport",
          "codebuild:UpdateReport",
          "codebuild:BatchPutTestCases",
          "codebuild:BatchPutCodeCoverages"
        ],
        Effect   = "Allow"
        Resource = [
          "arn:aws:codebuild:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:report-group/my-codebuild-project-*"
        ]
      },
      {
        Action = [
          "codebuild:BatchGetProjects",
          "codebuild:CreateProject",
          "codebuild:UpdateProject",
          "codebuild:DeleteProject",
          "codebuild:CreateWebhook",
          "codebuild:UpdateWebhook",
          "codebuild:DeleteWebhook",
        ]
        Effect   = "Allow"
        Resource = "arn:aws:codebuild:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:project/my-codebuild-project"
      },
    ]
  })
}

IAM Role

resource "aws_iam_role" "main" {
  name = "my-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "codebuild.amazonaws.com"
        }
      }
    ]
  })

  managed_policy_arns = [
    aws_iam_policy.main.arn
  ]
}

CodeBuild Project

GitHub のリポジトリ https://github.com/ORG/REPO.git はご自身のものに置き換えてください。

resource "aws_codebuild_project" "main" {
  name         = "my-codebuild-project"
  service_role = aws_iam_role.main.arn

  artifacts {
    type = "NO_ARTIFACTS"
  }

  environment {
    compute_type                = "BUILD_GENERAL1_SMALL"
    image                       = "hashicorp/terraform:1.5.6"
    type                        = "LINUX_CONTAINER"
    image_pull_credentials_type = "SERVICE_ROLE"
  }
  source {
    type                = "GITHUB"
    location            = "https://github.com/ORG/REPO.git"
    git_clone_depth     = 1
    report_build_status = true

    git_submodules_config {
      fetch_submodules = false
    }
  }
}

Webhook

Webhook は プルリクエストの作成・更新・再オープン、main と dev ブランチのプッシュに反応するようにしています。

参考ドキュメントは以下の通り。

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

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

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

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

  filter_group {
    filter {
      exclude_matched_pattern = false
      pattern                 = "PUSH"
      type                    = "EVENT"
    }
    filter {
      exclude_matched_pattern = false
      pattern                 = "^refs/heads/(main|dev)$"
      type                    = "HEAD_REF"
    }
  }
}

初回は Terraform をローカル PC などで実行する

初回だけローカル PC などで権限のある IAM ユーザで Terraform を実行をして、CodeBuild などのリソースを作成する必要があります。

初回は Webhook のリソース作成に失敗する

Terraform を実行すると、初回は、ResourceNotFoundException: Could not find access token for server type github エラーにより失敗します。この後の手順で、CodeBuild から GitHub へ接続をすればエラーが解消されます。

aws_codebuild_webhook.main: Creating...
╷
│ Error: creating CodeBuild Webhook: ResourceNotFoundException: Could not find access token for server type github
│ 
│   with aws_codebuild_webhook.codebuild,
│   on main.tf line 161, in resource "aws_codebuild_webhook" "main":
│  115: resource "aws_codebuild_webhook" "main" {

CodeBuild に保存する GitHub アカウントを決める

この後の GitHub への接続の手順を行うと、操作をしたときに GitHub にログインしていたアカウントが CodeBuild に保存されます。保存された GitHub アカウントを使って、CodeBuild から GitHub への連携が行われるので、どの GitHub アカウントで接続すればよいか、よく考えてから作業をしましょう。

特にチーム開発では、個人が開発に使用するアカウントではなく、CI などに使われる自動化のための machine user と呼ばれる GitHub アカウントを利用することが望ましいです。個人のアカウントで接続した場合、CodeBuild は個人の権限を使って、他のプライベートなリポジトリにアクセスすることができます。また、設定した人がチームから離れてリポジトリへのアクセス権限を失うと、CodeBuild は GitHub にアクセスできなくなります。

それに加え、GitHub アカウントはリポジトリの admin 権限を持つ必要があります。先程の Terraform で Webhook の作成に失敗しましたが、これを成功させるためにはリポジトリの admin 権限が必要です。

CodeBuild から GitHub に接続する

  1. 一つ前の手順で決めたアカウントで GitHub にログインしておきます。
  2. AWS マネジメントコンソールより、Terraform により作成された CodeBuild のプロジェクトへ移動します。
  3. 右上の Edit ボタンから Source を選択します。
  4. Connect using OAuth が初期選択されていることを確認し、Connect to GitHub ボタンをクリックします。
  5. GitHub の OAuth の認証画面が表示されるのでウィザードを進めます。GitHub の Organization Owner の許可が必要な場合があります。
  6. 接続に成功すると、You are connected to GitHub using OAuth.と表示されます。

再び Webhook のリソースを作成する

Terraform をもう一度実行して、さきほど作成に失敗したWebhook を作成します。成功すると AWS の Webhook のリソースが作成されることに加え、GitHub のリポジトリに Webhook が登録されます。

繰り返しになりますが、GitHub への接続に使用したアカウントは、リポジトリへの admin 権限を持つ必要があります。admin 権限が無い場合、以下のように Repository not found or permission denied エラーが発生して、Terraform の実行に失敗します。

aws_codebuild_webhook.main: Creating...
╷
│ Error: creating CodeBuild Webhook: OAuthProviderException: Failed to create webhook. Repository not found or permission denied. Please make sure the source credentials associated with your project have the permission to create webhook.
│ 
│   with aws_codebuild_webhook.codebuild,
│   on main.tf line 161, in resource "aws_codebuild_webhook" "main":
│  161: resource "aws_codebuild_webhook" "main" {

buildspec.yaml

buildspec.yaml では、以下のことをできるようにします。

  • terraform init
  • terraform validate
  • GitHub のプルリクエストを作成したら、分岐元のブランチに応じて workspace を切り替え terraform plan コマンドを実行
  • GitHub のプルリクエストがマージされたら、変更されたブランチに応じて workspace を切り替え terraform plan/apply コマンドを実行

いろいろ試行錯誤したのですが、webhook のイベントによって CodeBuild の環境変数が変わるので、まず最初に webhook イベントの種類を判断し、その後ブランチを見て分岐させる形にすると柔軟に対応ができました。コードは冗長な部分もあるのでもう少し短くできそうですが、管理するブランチや workspace が増えてしんどくなってきたら考え直します。運用しているとマネジメントコンソールから手動でビルドを実行したくなることもあるので、Build with override と Rebuild にも対応できるようにしました。

version: 0.2
phases:
  pre_build:
    commands:
      - terraform init
      - terraform validate
      - |
        CURRENT_BRANCH="$(git symbolic-ref HEAD --short 2>/dev/null)"
        if [ "$CURRENT_BRANCH" = "" ] ; then
          CURRENT_BRANCH="$(git rev-parse HEAD | xargs git name-rev | cut -d' ' -f2 | sed 's/remotes\/origin\///g')";
        fi
      - echo "CURRENT_BRANCH=$CURRENT_BRANCH"

  build:
    commands:
      - |
        if [[ "${CODEBUILD_WEBHOOK_EVENT}" =~ '^PULL_REQUEST' ]]; then
          echo "This event looks like a Pull Request"
          if [ "$CODEBUILD_WEBHOOK_BASE_REF" = "refs/heads/main" ]; then
            echo "Terraform will plan for prod workspace"
            terraform workspace select prod
            terraform plan -var-file="prod.tfvars" -input=false
          fi
        elif [[ "${CODEBUILD_WEBHOOK_EVENT}" == 'PUSH' ]]; then
          echo "This event looks like a Push"
          if [ "${CODEBUILD_WEBHOOK_HEAD_REF}" == "refs/heads/main" ]; then
            echo "Terraform will plan and apply for prod workspace"
            terraform workspace select prod
            terraform plan -var-file="prod.tfvars" -out=tfplan -input=false
            terraform apply -auto-approve -input=false tfplan
          fi
        elif [ "$CODEBUILD_SOURCE_VERSION" = "main" ]; then
          echo "This event looks like a Build with override"
          echo "Terraform will plan and apply for prod workspace"
          terraform workspace select prod
          terraform plan -var-file="prod.tfvars" -out=tfplan -input=false
          terraform apply -auto-approve -input=false tfplan
        elif [ "$CURRENT_BRANCH" = "main" ]; then
          echo "This event looks like a Rebuild"
          echo "Terraform will plan and apply for prod workspace"
          terraform workspace select prod
          terraform plan -var-file="prod.tfvars" -out=tfplan -input=false
          terraform apply -auto-approve -input=false tfplan
        fi

buildspec.yaml は インラインか S3 に保存することも検討を

AWS CodeBuild でのWebhook の使用のベストプラクティス - AWS CodeBuild に書かれているように、buildspec.yaml は S3 か CodeBuild Project にインラインで保存することもご検討ください。

たとえば今回の設定ではプルリクエストでビルドが実行されるので、buildspec.yaml を terraform destroy コマンドを実行するように書き換えてプルリクエストを作ると、プルリクエストをトリガにビルドが実行され Terraform で管理しているリソースを削除することができてしまいます。

Terraform のバージョンアップの際は 2 回プルリクエストの実行が必要

Terraform のバージョンアップのときは以下の手順を踏むことになります。仕組み上、仕方がないのですが、1 回のプルリクエストで済ませたかったです。

  1. Terraform コードで CodeBuild Project の terraform のコンテナのバージョン hashicorp/terraform:1.5.6 を変更をしてコミット、プッシュする
  2. プルリクエストを作成、マージ
  3. CodeBuild により Terraform が実行されて、CodeBuild project の terraform のコンテナのバージョンが変わる
  4. empty commit をしてもう一度プルリクエストを作成、マージ
  5. 新しい terraform のバージョンで terraform が実行される

手順 4, 5 を CodeBuild の Rebuild に置き換えることもできるので、お好みで。

-技術ブログ
-