mazyu36の日記

某SIer所属のクラウドエンジニアのブログ

CodeBuildのbuildspecはCDKのBuildSpec.fromObjectで作成するようにした

タイトルの通りですが、AWS CDKを使い始めてからAWS CodeBuildのbuildspecはBuildSpec.fromObjectを使って生成しています。

個人的にはbuildspecの管理がかなり便利だと思っているので、簡単にまとめます。

よくある(と思われる)buildspecの管理

buildspec.ymlソースコードリポジトリのルートに配置し、CodePipelineでCI/CDを行うというのはよく取られる手段かと思います。

docs.aws.amazon.com

ただ個人的には以下のようなつらみがありました。

つらみ1. アプリとインフラの責務による問題

プロジェクトによってはアプリ担当とインフラ担当(AWS)が分かれていることは少なくないと思います。

この時buildspec.yamlの管理がインフラ担当の役割の場合、リポジトリ内にアプリ担当とインフラ担当の資材が混ざる形になります。

そのため buildspec.ymlだけを更新する場合でもアプリの資材のリポジトリに変更を加えるため、プロジェクトのルール次第では承認等が必要となり更新のハードルが高くなることがあります。

あとは個人的な好みにもなりますが、このようなケースで責務が異なる資材を1リポジトリに入れるというのが若干抵抗あります。

つらみ2. 試行錯誤が面倒。また変更の反映漏れが起きる可能性あり。

動作検証など試行錯誤するときにbuildspec.ymlを更新・プッシュしてパイプラインを動かして試す、というのをよくやっていました。ただ時間がかかったり、後でコミットログをきれいにする必要があったりなど、手間がかかるのであまり好きではありませんでした。

そのため手動でbuildspecを作成して試行錯誤し、完成したらbuildspec.ymlに反映する、ということもやっていましたが、反映する時にミスったり反映漏れを起こしたりすることがありました。

つらみ3. 環境差分への対応が面倒

デプロイ先の環境(開発環境、本番環境etc)で実行コマンドを変えたいケースはよくあります。

この時、取り得る手段としては「環境ごとにbuildpspec.ymlを分ける(buildspec_dev.ymlなど)」や「buildspec内で実行コマンドを切り替える」あると思います。

前者を採用すると、リポジトリ内に複数のbuildspec.ymlができる形になります。ただgit-flowを採用しているとイマイチな運用になることが多かったです。

例えば試験環境のbuildspec(buildspec_stg.yml)を更新したい場合だと、以下のようにイマイチな運用になります。

  • developブランチでbuildspec_stg.ymlを更新する。この時、開発環境(develop)に対するCI/CDが無駄に動いてしまう。
  • releaseブランチにマージしたタイミングで試験環境に対してCI/CDが行われる。
  • mainにマージした場合は本番環境に対してCI/CDが無駄に動いてしまう。

各環境に対するbuildspecは対応するブランチのみに作成するのもやったことはありますが、リポジトリに運用が複雑になるので難点が多いです。

上記のため「buildspec内で実行コマンドを切り替える」という手段をとり、buildspecは1ファイルに集約することもありましたが、その場合は4のような問題があります。

つらみ4. シェル芸になり可読性が落ちる、デバッグが辛い

例えば本番環境のみ特定のコマンドを実行したい場合など、条件によりコマンドを切り替えたい場合はbuildspecで条件式を使うのが一案です。

以下はBuildにおいて、環境がprdとそれ以外で実行コマンドを切り替えている場合の例です。

{
  "version": 0.2,
  "phases": {
    "pre_build": {
      "commands": [
        "echo \"Pre Build\""
      ]
    },
    "build": {
      "commands": [
        "# Build Command",
        "if [ $ENV_NAME = \"prd\" ]; then echo \"Build prd\"; else echo \"Build non-prd\"; fi"
      ]
    },
    "post_build": {
      "commands": [
        "echo \"Post Build\""
      ]
    }
  },
  "artifacts": {
    "files": []
  }
}

このやり方もできなくはないですが、シェル芸になりがちなのが個人的に辛いです。

まず可読性が落ちます。定義ファイル自体が読みづらくなるのと、以下のようにCodeBuildの実行ログでもif文含めて丸ごと出力されるので、どのコマンドが実行されたか判別しづらいです。

そのため条件分岐を行う場合は、毎回echoでその後実行するコマンドを吐き出すということをよくやっていました。

[Container] 2023/03/10 06:53:45 Running command if [ $ENV_NAME = "prd" ]; then echo "Build prd"; else echo "Build non-prd"; fi
Build prd

またbuildspec自体のデバッグが面倒になるという問題もあります。

可読性やデバッグの難易度を考えると、単一のコマンド実行でなるべく済ませたいところです。

CDKによるbuildspecの管理

前置きが長くなりましたが本題です。

CDKで個人的によく使っている手段としてばbuildspecをオブジェクトとして生成し、BuildSPec.fromObjectでCodeBuildプロジェクトに渡す形で定義するというやり方です。

以下実装イメージです。

    // buildspecをオブジェクトとして生成
    const buildSpecObject = {
        version: 0.2,
        phases: {
            pre_build: {
                commands: [
                    'echo "Pre Build"',
                ]
            },
            build: {
                commands: [
                    '# Build Command',
                    'echo "Build prd"',
                ]
            },
            post_build: {
                commands: [
                    'echo "Post Build"',
                ]
            }
        },
        artifacts: {
            files: [
            ]
        }
    }

    // CodeBuildプロジェクト
    const project = new codebuild.PipelineProject(this, 'CodeBuildProject', {
        projectName: 'BastionBuildPrCodeBuildProjectoject',
        buildSpec: codebuild.BuildSpec.fromObject(buildSpecObject),  // fromObjectでbuildspecのオブジェクトを渡す。
        environment: {
            privileged: true,
            buildImage: codebuild.LinuxBuildImage.STANDARD_6_0 // CDK v2.68.0時点だとデフォルトがLinuxBuildImage.STANDARD_1_0なので注意
        },
        cache: codebuild.Cache.local(codebuild.LocalCacheMode.DOCKER_LAYER, codebuild.LocalCacheMode.CUSTOM)
    })

※本題と少しずれますがコメントに記載している通り、buildImageのデフォルトがSTANDARD_1_0(非推奨)なので注意が必要です。以下を参考に適切なイメージを設定した方が良いです。

docs.aws.amazon.com

上記のようにCDKでbuildspecを管理するようにしたところ以下のようなメリットがありました。

メリット1. インフラのリソース(CDKプロジェクト)として管理が行える

アプリのソースコードリポジトリbuildspec.ymlを置く必要がなくなります。アプリとインフラでチームが分かれている場合は責務によって管理単位を分割できるので都合が良くなります。

メリット2. hotswapによる高速デプロイで試行錯誤が容易

CDKのhotswapにより、buildspec変更による試行錯誤が高速で行えます。

cdk deploy時に--hotswapを付与することで高速にデプロイが可能です(裏側ではAPIを直接callしているため、CloudFormationを経由するオーバーヘッドがなく高速にデプロイができます)。

ただしドリフトが発生するので本番環境等では非推奨です。試行錯誤中は--hotswapで行い、最終的に実装完了後はcdk deployで反映とした方が良いかと思います。

hotswapが使える対象は以下に記載されています。Lambdaが例としてよく取り上げられる印象ですが、CodeBuildプロジェクトも対応しています。

github.com

以下のように数秒単位でデプロイが終わります。開発中は非常に便利です。

このためCDKのコードとbuildspecの実態を一致させながら高速に試行錯誤が行えます(時間がかかるから手動変更で試行錯誤し、コードに反映し忘れる等を防止できる)。

% cdk deploy --hotswap

✨  Synthesis time: 2.88s

⚠️ The --hotswap and --hotswap-fallback flags deliberately introduce CloudFormation drift to speed up deployments
⚠️ They should only be used for development - never use them for your production Stacks!

CdkFargateBastionStack: building assets...

[0%] start: Building 2332a8953f2d92ebffdc01cf20d5a2fb5bf2ef29764cda4186f01c55edee8c73:current_account-current_region
[0%] start: Building 9a1894465f2952548f5c1b0103a933bf4ce94954d87336e1ad54e2715f4ea673:current_account-current_region
[50%] success: Built 2332a8953f2d92ebffdc01cf20d5a2fb5bf2ef29764cda4186f01c55edee8c73:current_account-current_region
[100%] success: Built 9a1894465f2952548f5c1b0103a933bf4ce94954d87336e1ad54e2715f4ea673:current_account-current_region

CdkFargateBastionStack: assets built

CdkFargateBastionStack: deploying... [1/1]
[0%] start: Publishing 2332a8953f2d92ebffdc01cf20d5a2fb5bf2ef29764cda4186f01c55edee8c73:current_account-current_region
[0%] start: Publishing 9a1894465f2952548f5c1b0103a933bf4ce94954d87336e1ad54e2715f4ea673:current_account-current_region
[50%] success: Published 2332a8953f2d92ebffdc01cf20d5a2fb5bf2ef29764cda4186f01c55edee8c73:current_account-current_region
[100%] success: Published 9a1894465f2952548f5c1b0103a933bf4ce94954d87336e1ad54e2715f4ea673:current_account-current_region

✨ hotswapping resources:
   ✨ CodeBuild Project 'CodeBuildProject'
✨ CodeBuild Project 'CodeBuildProject' hotswapped!

 ✅  CdkFargateBastionStack

✨  Deployment time: 1.51s

Stack ARN:
arn:aws:cloudformation:ap-northeast-1:024532196973:stack/CdkFargateBastionStack/34f7ce40-c09c-11ed-b0f6-0e5e5ec62f59

✨  Total time: 4.4s

メリット3. 環境差分への対応が容易にできる

buildspecを環境ごとに切り替えるのが容易になります。

例えば「4. シェル芸になり可読性が落ちる、デバッグが辛い」で示したbuildspecのように、簡易な切替の場合は三項演算子で実現可能です。

    // buildspecをオブジェクトとして生成
    const buildSpecObject = {
        version: 0.2,
        phases: {
            pre_build: {
                commands: [
                    'echo "Pre Build"',
                ]
            },
            build: {
                commands: [
                    '# Build Command',
                    isPrd ? 'echo "Build prd"' : 'echo "Build non-prd"',  // 三項演算子で切り替え
                ]
            },
            post_build: {
                commands: [
                    'echo "Post Build"',
                ]
            }
        },
        artifacts: {
            files: [
            ]
        }
    }

環境差分が一部の場合は上記のように簡単に済ませることが多いです。しかし環境差分がたくさんある場合は、この方法だとCDKのコード自体の可読性が悪くなってしまう可能性があります。

そのような場合は割り切ってbuildspecのオブジェクト自体を別にすることが多いです。

    // 試験環境用のbuildspec
    const buildSpecObjectStg = {
        version: 0.2,
        phases: {
            // 略
        }
    }

    // 本番環境用のbuildspec
    const buildSpecObjectPrd = {
        version: 0.2用
        phases: {
            // 略
        }
    }

いずれにしてもCDKでデプロイされたCodeBuild上のbuildspecにおいては、条件分岐等の記載が不要になりすっきりします。そのためシェル芸も削ることができます。

以下は上記2つのいずれかの方法を元に本番環境向けにデプロイしたbuildspecの例です。Buildのコマンドで条件分岐の記述がなくなり、prd用のコマンドだけにすることができます。

これにより実行コマンドが単一になり、CodeBuildのログが見やすくなる、デバッグが容易になる、というメリットがあります。

{
  "version": 0.2,
  "phases": {
    "pre_build": {
      "commands": [
        "echo \"Pre Build\""
      ]
    },
    "build": {
      "commands": [
        "# Build Command",
        "echo \"Build prd\""
      ]
    },
    "post_build": {
      "commands": [
        "echo \"Post Build\""
      ]
    }
  },
  "artifacts": {
    "files": []
  }
}

そのほか

buildspecはCDKで管理したいけど、yaml等で書きたい場合はBuildSpec.fromAssetを使うのが良いかと思います。

これを使えばyamlで書いたbuildspecのファイルをCDKのリポジトリに置いておき、デプロイ時にそこから読み込むということが可能です。また複数環境分用意しておいて、デプロイ時に読み込む対象を変えるということも容易かと思います。