CodePipelineとCodeBuildでECRのイメージを量産する

この記事は「Medley (メドレー) Advent Calendar 2024」の23日目の記事です。

https://qiita.com/advent-calendar/2024/medley

---

# はじめに
今年の10月から株式会社メドレーの人材プラットフォーム本部SREチームにアサインした[@gongon282828](https://qiita.com/gongon282828)です。

さて、みなさん！
突然ですが、カスタムイメージ、使っていますか??

LambdaやCodeBuildでカスタムイメージを利用することで、 CI/CDの高速化やruntime環境内のライフサイクルをコントロールできたりと色々と便利です。

ただ、そのメリットは理解しつつも、カスタムイメージやECRを手配したりするのも面倒だったりします。

そこで今回は、CodePipelineとCodeBuildを利用して、DockerFileと変数を修正するだけで、ECRのイメージを量産する方法を紹介します。

※CodePipelineとCodeBuildでCodebuildで利用するカスタムイメージを作るので少しややこしいですね...

# 前提

- 本記事ではTerraformでの環境構築の方法の紹介になります。
- AWSのリソースに利用する一部変数ついては、可読性を高めるために本来は`variable.tf`等で他のmoduleから値を参照するところを、localで変数化したものを参照させています。
- 記事が冗長してしまうことを防ぐ目的で、IAMポリシーの作成とリポジトリ連携周りの構築については、省略してます。

では早速、具体的な構築方法について詳説します。

# 構築するリソース各種

ここからは実際に、Terraformで実際の環境を構築する方法を説明します。
以下、構築を行います。

1. 作成したイメージを保管するECRの構築
1. カスタムイメージを構築するCodeBuildの作成
1. `buildspec.yml`ファイルの作成
1. コンソールから変数入力による作成するイメージの指定ができるCodePipelineの作成

(4)のCodePipeline構築では、DockerFileを保管しているリポジトリの取り込みも行います。

## 作成したイメージを保管するECRの構築

ここでは、量産するカスタムイメージの保管場所ECRをTerraformで構築します。

```terraform
locals{
  custom_build_images = ["IMAGE_A","IMAGE_B"]
 }

resource "aws_ecr_repository" "main" {
  count = length(local.custom_build_images)
  name  = "${local.custom_build_images[count.index]}"
  image_tag_mutability = "MUTABLE"

  image_scanning_configuration {
    scan_on_push = true
  }
  
  tags = {
    lifecycle-flag = "lifecycle-flag-${local.custom_build_images[count.index]}"
  }
}

resource "aws_ecr_lifecycle_policy" "main" {
  count = length(local.custom_build_images)
  
  repository = aws_ecr_repository.main[count.index].name

  policy = jsonencode(
    {
      rules = [
        {
          action = {
            type = "expire"
          }
          description  = "Keep last 15 images"
          rulePriority =  1
          selection = {
            countNumber   = 15
            countType     = "imageCountMoreThan"
            tagStatus     = "tagged"
            tagPrefixList = ["lifecycle-flag-"]
          }
        },
      ]
    }
  )
}
```

解説：
1. ローカル変数で定義した`custom_build_images`を`aws_ecr_repository`のcountで回しています。この`custom_build_images`の配列を増やすことで、追加したいカスタムイメージ用のECRを増やせます。
2. `aws_ecr_lifecycle_policy`でECRのライフサイクルを設定しています。入れ忘れてしまったりするところなので、注意しましょう。

これでECRの構築は完了です。ECR自体はいくらでも構築する機会があるので、そこまで難しくない内容だと思います。

## カスタムイメージを構築するCodeBuildの作成

ここでは、カスタムイメージを構築するためのCodeBuildを作成します。

```terraform
locals{
  account_id         = "xxxxxxxxxxxx"
  region             = "ap-northeast-1"
  vpc_id             = "x.x.x.x/16"
  subnet_ids         = ["x.x.0.x/24","x.x.1.x/24"]
  security_group_ids = ["sg-xxxxxxxxxxx"]
}

resource "aws_codebuild_project" "main" {
  name           = "custom-build-image"
  build_timeout  = 15
  queued_timeout = 5
  badge_enabled  = false
  # roleの構築については便宜上以下の形で記載
  service_role   = aws_iam_role.codebuild.arn

  environment {
    compute_type                = "BUILD_GENERAL1_LARGE"
    image                       = "aws/codebuild/amazonlinux2-x86_64-standard:5.0"
    type                        = "LINUX_CONTAINER"
    image_pull_credentials_type = "CODEBUILD"
    privileged_mode             = true
    
    environment_variable {
      name  = "AWS_ACCOUNT_ID"
      value = "${local.account_id}"
      type  = "PLAINTEXT"
    }
    
	environment_variable {
      name  = "AWS_DEFAULT_REGION"
      value = "${local.region}"
      type  = "PLAINTEXT"
    }
  
  }

  source {
    type      = "CODEPIPELINE"
    buildspec = file("${path.module}/buildspec.yml")
  }

  artifacts {
    type = "CODEPIPELINE"
  }

  vpc_config {
    vpc_id              = local.vpc_id
    subnets             = local.subnet_ids
    security_group_ids  = [local.security_group_ids]
  }
}
```

解説：
1. `environment_variable`は必要最小限にとどめていますが、必要に応じて環境変数を追加してください。
1. `source`はmodule内の同一階層に設置します。
1. `artifacts`はイメージの量産をコントロールするためにCodePipelineを指定しています。

 ここでは、`vpc_config`も設定していますが、必須ではありません。ちなみに、VPCエンドポイントからS3への通信費を削減することができます。
 
## `buildspec.yml`ファイルの作成

ここでは、CodeBuildで実行するための`buildspec.yml`を作成します。
Dockerのbuild実行、イメージタグ付与、ECRへのpushの処理もここで行います。

```yml
version: 0.2

env:
  exported-variables:
    - AWS
    - AWS_ACCOUNT_ID
    - AWS_DEFAULT_REGION

phases:
  install:
    run-as: root
    commands:
      - aws ecr get-login-password --region ${AWS_DEFAULT_REGION} |
        docker login --username AWS --password-stdin ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com
  build:
    run-as: root
    commands:
      - docker build -t ${CUSTOM_BUILD_IMAGE_TARGET}:latest
        -t ${CUSTOM_BUILD_IMAGE_TARGET}:${CODEBUILD_RESOLVED_SOURCE_VERSION}
        -f ./modules/image_build/Dockerfile/Dockerfile_${CUSTOM_BUILD_IMAGE_TARGET} . --progress plain
      - docker tag ${CUSTOM_BUILD_IMAGE_TARGET}:${CODEBUILD_RESOLVED_SOURCE_VERSION}
        ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com/${CUSTOM_BUILD_IMAGE_TARGET}:${CODEBUILD_RESOLVED_SOURCE_VERSION}
      - docker tag ${CUSTOM_BUILD_IMAGE_TARGET}:latest
        ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com/${CUSTOM_BUILD_IMAGE_TARGET}:latest
  post_build:
    run-as: root
    commands:
      - docker push ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com/${CUSTOM_BUILD_IMAGE_TARGET}:latest
      - docker push ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com/${CUSTOM_BUILD_IMAGE_TARGET}:${CODEBUILD_RESOLVED_SOURCE_VERSION}
      - 
```

解説：
1. `install`部分で、ECRにログインします。ECRへアクセスできる権限は、過不足なく行う必要があります。
1. `build`部分で、実際のbuildとtag付けを行います。カスタムイメージで参照する際に最新版を参照できるように、latestタグを付与しています。また、バージョンを追いかけられるように`CODEBUILD_RESOLVED_SOURCE_VERSION`も設定しています。
1. `CUSTOM_BUILD_IMAGE_TARGET`で後から構築するCodePipelineで任意の更新したいDockerFileを指定できるようにします。また、Build元のDockerFileも`Dockerfile_${CUSTOM_BUILD_IMAGE_TARGET}`で指定しています。
1. ここでは、`./modules/image_build/Dockerfile/`ディレクトリ配下に量産用の`DockerFile`を保管しています。そのため、CodePipelineでのソース取り込みは、必然的にTerraformのソースコード保管リポジトリから引っ張ってくることになります。

肝となっているのが、`CUSTOM_BUILD_IMAGE_TARGET`の変数により、一つのCodeBuildで任意のカスタムイメージを作成できるようにしている点です。

## コンソールから変数入力によるイメージの指定ができるCodePipelineの作成

ここではCodePipelineを作成します。
AWSコンソール上からCodePipeline手動実行時に、CodeBuildへ環境変数を渡すことにより、任意のイメージを指定して新規イメージ作成や更新を行えるようになります。

```terraform
locals{
  connection_arn     = "xxxxxx.arn"
  full_repository_id = "HOGE/TERRAFIRN_REPO"
  branch_name        = "main"
}

resource "aws_codepipeline" "main" {
  name     = "custom-build-image"
  # roleの構築については便宜上以下の形で記載
  role_arn = aws_iam_role.codepipeline.arn

  pipeline_type = "V2"
 
  variable {
    name          = "CUSTOM_BUILD_IMAGE_TARGET"
    description   = "Specify the build image to create [IMAGE_A/IMAGE_B]"
    default_value = "null"
  }

  artifact_store {
    location = aws_s3_bucket.codepipeline.bucket
    type     = "S3"
  }

  stage {
    name = "Source"

    action {
      name             = "Source"
      category         = "Source"
      owner            = "AWS"
      provider         = "CodeStarSourceConnection"
      version          = "1"
      output_artifacts = ["source_artifact"]
      configuration = {
	    DetectChanges    = false
        # ソース管理リポジトリ指定と紐付けについては、便宜上以下の形で記載
        ConnectionArn    = "${local.connection_arn}"
        FullRepositoryId = "${local.full_repository_id}"
        BranchName       = "${local.branch_name}"
      }
    }
  }

  # Image Build
  stage {
    name = "Build"

    action {
      name             = "Build"
      category         = "Build"
      owner            = "AWS"
      provider         = "CodeBuild"
      input_artifacts  = ["source_artifact"]
      output_artifacts = ["build_artifact"]
      version          = "1"

      configuration = {
        ProjectName = "${aws_codebuild_project.main.name}"
     
        EnvironmentVariables = jsonencode([
          {
            type  = "PLAINTEXT"
            name  = "CUSTOM_BUILD_IMAGE_TARGET"
            value = "#{variables.CUSTOM_BUILD_IMAGE_TARGET}"
          }
        ])
 
      }
    }
  }

}

resource "aws_s3_bucket" "codepipeline" {
  bucket = "custom-build-image-codepipeline-artifacts"
}

resource "aws_s3_bucket_acl" "codepipeline" {
  bucket = aws_s3_bucket.codepipeline.id
  acl    = "private"
  depends_on = [aws_s3_bucket_ownership_controls.codepipeline]
}

resource "aws_s3_bucket_ownership_controls" "codepipeline" {
  bucket = aws_s3_bucket.codepipeline.id
  rule {
    object_ownership = "ObjectWriter"
  }
}
```

解説：
1. パイプラインタイプをv2に指定し、実行時にパイプラインレベルで変数を渡せるようにする必要があります。
1. `variable`で`CUSTOM_BUILD_IMAGE_TARGET`をAWSコンソール上から変数を入力して任意のDockerFileでbuildできるようにしています。また、`description`で入力内容を記載することで、他の作業者も更新しやすくしています。
1. `stage`の`Source`でリポジトリを引っ張ってこれるようにしています。
1. `stage`の`Build`の`EnvironmentVariables`で、`CUSTOM_BUILD_IMAGE_TARGET`を渡すようにしています。

ここでも出てきました、`CUSTOM_BUILD_IMAGE_TARGET`です。
パイプラインのV2は他にも便利な機能が追加されており、詳しくは公式ドキュメントに記載があります。

[適切なパイプラインのタイプの選択 - AWS CodePipeline](https://docs.aws.amazon.com/ja_jp/codepipeline/latest/userguide/pipeline-types-planning.html)

V2タイプからGitタグの生成を発火条件にできるようになったのが素敵ですね。

以上でTerraformでCodePipelineとCodeBuildでECRのイメージを量産する環境の構築方法になります。

# 補足：新規でDockerイメージを作成する方法

以下、3STEPで完了です。
1. `local.custom_build_images`の配列で新規で作りたい要素を追加し、`terraform apply`します。この追加した配列が`CUSTOM_BUILD_IMAGE_TARGET`の一つになります。
1. `./modules/image_build/Dockerfile/Dockerfile_${CUSTOM_BUILD_IMAGE_TARGET}`のDockerfileを用意します。
1. AWSコンソール上でのCodePipeline実行時に、新規で作った`CUSTOM_BUILD_IMAGE_TARGET`を指定して作成して完了です。

DockerFIleを用意するだけで、とても簡単にECRイメージを作れるようになりました！

# まとめ
今回は、CodePipelineとCodeBuildでECRのイメージを量産する方法について紹介させていただきました。一度量産できる環境を構築してしまえば、インフラにあまり精通していないメンバーでも気軽にイメージを構築でき、かつカスタムイメージの作成が突発的に必要になった場合でも迅速に対応することができます。

この記事がどなたかの助けになれば幸いです！

明日は、[@Daishu](https://qiita.com/Daishu)さんによるイベントの登壇についての記事になります！
乞うご期待です！