はじめに
この記事はLCL Advent Calendar 2021 - 2日目です。
バックエンドエンジニアの星野です。アドベントカレンダーを4日連続にしたことについて2日目ですでにギリギリです。
昨日の記事ではバッチジョブ実行基盤のパターンを紹介しましたので今日はその続きになります。
最初はCodeBuildのカスタムイメージです。 Fargateと比較した際のポイントやバッチジョブをTerraformから作成しやすくするための工夫について解説していきます。
CodeBuildをカスタムイメージで利用する
CodeBuildはAWSから提供されているDockerイメージの他にPublic RegistryやECRから任意のDockerイメージをビルド環境として使うことができます。 LCLが利用しているフレームワークであるRuby on Railsですとgemをインストール済みのDockerイメージを指定することでrails runnerやrakeタスクをすぐに実行することができます。 また、VPCをサポートしているためデータベースなどVPC内のリソースに接続するなどアプリケーションのビルドに囚われずなんでもできます。
CodeBuild VS Fargate
この用途ですとFargateを利用して単発のタスクを実行するの大きくは変わらないように聞こえます。 単発の実行においてCodeBuildがFargateよりも扱いやすいポイントを挙げていきます。
1. 実行コマンドの指定
CodeBuild: buildspec.ymlに記載
Fargate: Dockerfileに記載したentrypointやcommandまたはタスク定義によるそれらの上書き
Fargateの場合はDockerの作法にしたがってentrypointやcommandに実行コマンドを指定しますが1回の起動で複数のコマンドを実行する必要がある場合は別途シェルスクリプトなどを用意することがあるでしょう。 一方CodeBuildはbuildspec.ymlによって実行コマンドを連続して記載できるので追加ファイルなど不要で設定することができます。
2. コンテナの起動(コンソール)
CodeBuild: Start Buildボタン
Fargate: タスク定義の画面からDeployボタンを押下した後にクラスタやVPCの選択が必要
FargateはECSの性質上コンテナを起動する場合はタスク定義からクラスタやVPCの設定がする必要がある一方CodeBuildはボタンで一発です。 FargateでもStep Functionsから実行するなど工夫次第で解決することはできますがCodeBuildのみで設定が完結するのは大きいです。
3. コンテナの起動(CLI)
CodeBuild: aws codebuild start-build
に--project-name
をオプションで渡す
Fargate: aws ecs run-task
に--task-definition
に加えて--network-configuration
をオプションで渡す
run-taskのドキュメントには--network-configuration
が必須オプションではない記述ですがFargateはネットワークモードがawsvpc固定なので必須のオプションになります
コンソールと同様に1度作成してしまえばCodeBuildの方が少ないステップで起動まで進むことができます。
4. 実行履歴とログ出力
CodeBuild: 実行履歴は永続化される。ログはCloudWatch LogsかS3を選択可能でコンソール画面から見やすい
Fargate: 実行履歴は最低1時間保持された後に削除対象。ログはCloudWatch LogsまたはFireLensで柔軟に設定可能。コンソールの出来は新バージョンも後一歩な印象
実行履歴がFaragateは1時間以上経過すると削除されてしまうのに対しCodeBuildはデフォルトでは無制限に保存されます。 ログ出力は両者ともCloudWatch Logsが使えて、CodeBuildは他にS3のみ、FargateはFireLensで柔軟に設定可能なためここはFaragateが優れているポイントです。 ただしコンソールから参照する場合はCodeBuildはリアルタイムで表示できたりUIがこなれているためCodeBuildの方がデフォルトで扱いやすい印象です。 Fargateの場合は外部SaaSやAthenaなど多少の作り込みが必要なため要件やコストとのトレードオフなると思われます。
5. バッチビルド
CodeBuild: 指定したビルドの完了後に次のビルドを起動するバッチビルドを定義可能
Fargate: Step Functionsなど他のサービスと連携が必要
CodeBuildは単体でバッチビルドができます。
バッチビルドにより簡単なワークフローを作成することができます。Fargateは単体ではこのような機能は提供されていません。
6. EventBridgeに通知するイベント
CodeBuild: イベントにCloudWatch Logsのリンクを含む*1
Fargate: イベントにCloudWatch Logsのリンクは含まれない*2
細かいところですがCodeBuildは状態変更イベントをEventBridgeに通知する際にCloudWatch Logsのリンクを含めてくれます。 これはSlackに通知する際に役に立ちます。
7. 最小マシンスペックと価格
CodeBuild: 2vCPU/メモリ3GiB $0.3/hour*3
Fargate: 0.25vCPU/メモリ0.5GiB 約$0.014/hour + Savings Plansの対象 + Spotで最大70%オフ*4
いずれも記事執筆時点での東京リージョンの価格
CodeBuildは最小のスペックのオプションが大きめでスケールアップの選択肢が少なめかつ割引のオプションもないのでコスト面ではサイズを柔軟に選択可能なFargateの圧勝です。 ちなみに最大スペックはFargateが4vCPU/メモリ30GiBに対しCodeBuildは72vCPU/メモリ144GiBなので上限は勝利しています。
1~6までの付加価値とのトレードオフで適切に選択しましょう。
Terraformでバッチジョブを登録する
LCLではTerraformによるコード化を推進しているためバッチジョブの登録もTerraformから行います。 ここではTerraformに不慣れな開発者でも登録をしやすくする工夫を解説します。
resource "aws_codebuild_project" "batch_job" { for_each = local.batch_jobs_codebuild name = "batch-job-${each.value.name}" description = each.value.description build_timeout = each.value.build_timeout != null ? each.value.build_timeout : 60 service_role = aws_iam_role.codebuild.arn artifacts { type = "NO_ARTIFACTS" } environment { compute_type = each.value.compute_type != null ? each.value.compute_type : "BUILD_GENERAL1_SMALL" image = each.value.default_image != true ? "ecr_repository_url:prod" : "aws/codebuild/standard:5.0" type = "LINUX_CONTAINER" image_pull_credentials_type = "CODEBUILD" privileged_mode = true } logs_config { cloudwatch_logs { group_name = aws_cloudwatch_log_group.batch_job[each.key].name stream_name = "job" } } source { type = "NO_SOURCE" buildspec = templatefile(each.value.default_image != true ? "buildspec.yml.tpl" : each.value.template, { account_id = data.aws_caller_identity.current.account_id region = data.aws_region.current.name task = each.value.task }) } vpc_config { vpc_id = "vpc-id" subnets = ["subnet-id"] security_group_ids = ["security-group-id"] } } resource "aws_cloudwatch_log_group" "batch_job" { for_each = local.batch_jobs_codebuild name = "job-${each.value.name}" retention_in_days = 0 }
TerraformでCodeBuildプロジェクトを作成する定義はこのようになります。
最初のポイントはsourceのtypeを"NO_SOURCE"にしてbuildspecをtemplatefile関数で渡すところです。 このようにすることでterraformのrootモジュールに含めているbuildspec.yml.tplをテンプレートファイルとしてバッチジョブ毎に専用のbuildspec.ymlを生成します。
version: 0.2 env: shell: bash variables: RAILS_ENV: production RELEASE_STAGE: prod HOME: /app parameter-store: DATABASE_PASSWORD: /awesome-service/prod/app/rails/DATABASE_PASSWORD DATABASE_HOST: /awesome-service/prod/app/rails/DATABASE_HOST phases: pre_build: commands: - cd $ROOT_PATH build: run-as: rails commands: - ${task}
buildspec.yml.tplはバッチジョブ毎に${task}を上書きします。 後述するvariablesでrails runnerやrakeタスクのコマンドを渡します。
variable "awesome_job" { type = map(object({ name = string description = string task = string schedule = string enabled = bool build_timeout = optional(number) compute_type = optional(string) default_image = optional(bool) template = optional(string) })) default = { awesome-job = { name = "awesome-job" description = "すごいバッチジョブ" task = "rake awesome_job" schedule = "cron(0 0 * * ? *)" enabled = true } } } variable "great_job_with_addtional_config" { type = map(object({ name = string description = string task = string schedule = string enabled = bool build_timeout = optional(number) compute_type = optional(string) default_image = optional(bool) template = optional(string) })) default = { great-job-with-addtional-config = { name = "great-job-with-addtional-config" description = "すごい追加設定のジョブ" task = "rails runner Tasks::Great::Job" schedule = "cron(0 0 * * ? *)" enabled = true build_timeout = 480 compute_type = "BUILD_GENERAL1_2XLARGE" } } }
Terraformの1つのvariableに対して1つのジョブを対応させます。*5ポイントとしてOptional Object Type Attributesを利用しています。 これはterraform 0.14から実験的機能として使えるようになった設定で、文字通り任意で追加できるオプションのパラメータとして定義できます。
Optional Object Type Attributesを利用することでデフォルト値を上書きしたい場合だけattributeに追加します。
compute_type = each.value.compute_type != null ? each.value.compute_type : "BUILD_GENERAL1_SMALL"
例えばcompute_typeをこのように書くことでcompute_typeを明示的に指定した場合はその値を、無ければデフォルト"BUILD_GENERAL1_SMALL"を設定できます。CodeBuildではdefault_imageをtrueにすることでカスタムイメージではなくAWSマネージドなイメージで起動できるようにしたり、templateに専用のbuildspec.ymlのファイルを渡すことで特別な対応が必要な場合の設定を扱いやすくしています。
Optional Object Type Attributesは実験的機能で今後インターフェースが変更される可能性があり利用については注意しましょう。
locals { batch_jobs_codebuild = merge( var.awesome_job, var.great_job_with_addtional_config ) }
最後に複数のvariableをlocalsで一つのMap型にまとめてfor_eachで回します。 この一連のサンプルコードではvariableのscheduleとenabled attributeを利用していませんがEventBridgeから定期実行するリソースもCodeBuildプロジェクト毎に作成しています。
駆け足での説明になりましたが、このように定義することで開発者がTerraformのことに詳しくなくてもvariableとlocalsをコピペして追加していくだけでバッチジョブを量産できる体制にしています。
まとめ
CodeBuildをバッチジョブの実行基盤として使う場合のポイントを見ていきました。 CodeBuildの運用体験は強力なものがあるので財布と相談しながら是非一度試してみてください。
採用情報
LCLでは開発メンバーを募集しております!
もし興味をお持ちになりましたらお気軽に応募してください。
*1:https://docs.aws.amazon.com/ja_jp/codebuild/latest/userguide/sample-build-notifications.html#sample-build-notifications-ref
*2:https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/ecs_cwe_events.html#ecs_task_events
*3:https://aws.amazon.com/codebuild/pricing/
*4:https://aws.amazon.com/fargate/pricing/
*5:TerraformはString型はダブルクォート必須なのでtaskの文字列をエスケープしなくてもいいようにRubyの場合は呼び出すコマンドの引数はキーワードで取るようにしておくと扱いやすいです