なになれ

IT系のことを記録していきます

AWS環境におけるモノレポでのDockerイメージのビルド方法

調べた限り、モノレポでDockerイメージをいい感じにビルドする方法が見つからなかったので実装してみました。
AWS環境が前提で、CodeBuildを使ってDockerイメージをビルドする想定です。
GitHub Actionsを使えばもっとスマートに実装できるかもしれませんが、外部のSaaSAWSの認証情報を持たせたくないという考えがあり、AWS内で完結させました。
本内容はAWSに限らずにモノレポでのDockerイメージのビルドについて、参考になる気がしています。

本内容のソースは以下にあります。

github.com

ディレクトリ構成

monorepo-app1とmonorepo-app2がモノレポ内に存在する各アプリケーションです。

monorepo-container-build-example
├── buildspec.yml
├── monorepo-app1
│   ├── Dockerfile
│   ├── index.js
│   └── package.json
├── monorepo-app2
│   ├── Dockerfile
│   ├── index.js
│   └── package.json
├── scripts
│   └── build.sh
└── terraform
    ├── codebuild.tf
    └── main.tf

CodeBuildの環境はTerraformで構築するようにしました。Terraformのリソースはterraformディレクトリにあります。
buildspec.ymlはCodeBuildが実行するビルド内容のファイルです。buildspec.ymlでは、scripts/build.shのシェルスクリプトを実行します。

実装内容

CodeBuild

CodeBuildの処理内容が今回のポイントなので、それについて説明します。
ビルドのやり方としては、モノレポに存在する各アプリケーションのビルドを実行するために、全てのアプリケーションをビルド対象にします。
その上で、レジストリに存在するイメージと差異があった場合のみ、イメージをレジストリにPushします。
以降が詳しい解説です。

まずは、buildspec.ymlの解説です。

buildspec.yml

version: 0.2

env:
  variables:
    DOCKER_BUILDKIT: 1
  parameter-store:
    REGISTRY_URI: "/ecr/registry_uri"
phases:
  pre_build:
    commands:
      - aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin $REGISTRY_URI
      - image_tag=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)
  build:
    commands:
      - ${CODEBUILD_SRC_DIR}/scripts/build.sh

ECRのレジストリURIを指定するのにSession Managerのパラメータストアを使用していますが、ベタ書きでも問題ないです。
image_tagはDockerイメージのタグ値になります。GitのコミットIDの先頭7文字をタグ値にします。
これ以降は、scripts/build.shを実行します。

build.shの解説です。

scripts/build.sh

#!/bin/sh -x

target_dirs=`find . -type f -name "Dockerfile" -exec dirname {} \; | sort -u`
for target in ${target_dirs}
do
  container_name=$(echo $target | awk '{i=split($0,array,"/");print array[i]}')
  docker_filepath=${target}
  repository_uri=${REGISTRY_URI}/${container_name}
  aws ecr create-repository --repository-name ${container_name} --region ap-northeast-1 || true
  docker pull $repository_uri:latest || true
  docker build --cache-from $repository_uri:latest --build-arg BUILDKIT_INLINE_CACHE=1 -t $repository_uri:$image_tag -f ${docker_filepath}/Dockerfile ${docker_filepath}
  image_digest=$(docker images $repository_uri --format "{{.Tag}}\t{{.Digest}}" | awk '$1 == "'"${image_tag}"'" {print $2}')
  if [ "${image_digest}" == "<none>" ]; then
    docker tag $repository_uri:$image_tag $repository_uri:latest
    docker push $repository_uri:$image_tag
    docker push $repository_uri:latest
  fi
done

findコマンドでDockerfileが存在するディレクトリを見つけます。ここでは、monorepo-app1とmonorepo-app2が見つかります。それぞれのDockerfileがビルド対象になります。
findコマンドの出力値はディレクトリのパスになるので、container_nameの処理でディレクトリ名だけを抽出します。
docker pull $repository_uri:latest || trueで最新のDockerイメージを取得します。
docker build --cache-fromでそのイメージを指定することで、キャッシュとして最新のDockerイメージを使用した上でイメージのビルドを行います。
この時にイメージに変更がなければ、イメージのダイジェスト値があります。変更があれば、ダイジェスト値はありません。
イメージのダイジェスト値はレジストリにPushすることで付与される値です。
イメージのダイジェスト値がない場合、最新のイメージと差異があり、レジストリにPushする必要があるイメージになります。
if [ "${image_digest}" == "<none>" ]; thenでダイジェスト値の有無を確認し、docker push $repository_uri:$image_tagレジストリにPushを行います。
latestのタグが今回のビルドの仕組みでは必要になるので、docker push $repository_uri:latestも行います。

Terraform

CodeBuildのプロジェクトを作成するTerraformのコードについても解説します。

codebuild.tf

...
resource "aws_codebuild_project" "container_build" {
  name = "container-build"
  artifacts {
    type = "NO_ARTIFACTS"
  }
  cache {
    modes = [
      "LOCAL_DOCKER_LAYER_CACHE",
    ]
    type = "LOCAL"
  }
  environment {
    compute_type    = "BUILD_GENERAL1_SMALL"
    image           = "aws/codebuild/amazonlinux2-x86_64-standard:3.0"
    privileged_mode = true
    type            = "LINUX_CONTAINER"

  }
  source {
    type            = "GITHUB"
    location        = "https://github.com/hi1280/monorepo-container-build-example.git"
    git_clone_depth = 1
  }
  service_role  = aws_iam_role.container_build.arn
  build_timeout = 30
}

resource "aws_codebuild_webhook" "container_build" {
  project_name = aws_codebuild_project.container_build.name

  filter_group {
    filter {
      exclude_matched_pattern = false
      pattern                 = "PUSH, PULL_REQUEST_MERGED"
      type                    = "EVENT"
    }
    filter {
      exclude_matched_pattern = false
      pattern                 = "refs/heads/master"
      type                    = "HEAD_REF"
    }
  }
}

aws_codebuild_projectはCodeBuildを作成するTerraformリソースです。
sourceにDockerイメージのビルドを行うリポジトリを指定します。
service_roleにはIAMロールを指定します。ECRへのIAM権限が必要です。
aws_codebuild_webhookはCodeBuildのWebhookの指定です。
いずれかのファイルがGitリポジトリにコミットされるとCodeBuildが自動的に実行されるように設定します。

まとめ

イメージのダイジェスト値を使うことで無駄にイメージをPushすることなく、モノレポでイメージをビルドできました。
ただ毎回モノレポ上の全てのアプリケーションのイメージをPullしていて、無理矢理な方法だと思うので、ほかにもっとスマートなやり方があれば教えてほしいです。