ECSに合わせたCodePipelineについて、めちゃめちゃ詳しく書いてみた (AWS, CI/CD)
よくAWSに触れるものです。
皆さんはCI/CD環境にどのSaaSを使っていますか?
Circle CIやTravis CI、jenkinsなどは有名ですが、AWSにもとっても便利なCI/CDであるCodePipelineが存在します!
インフラストラクチャとしてECSを用いているユースケースに対応したドキュメントや一般記事、Cloudfromationで構築している読み物が少なかったので、そのあたりを実際に構築してみて、ポイントを深掘って書いていきたいと思います。
実際に業務で開発しているサービスにスポットを当てています。
CodePipelineについて
CodePipelineはCodeCommit、CodeBuild、CodeDeployなどを用いて構築するCI/CDサービスで、インフラストラクチャでAWSを用いている場合には特に効果が発揮されます。
ビルドの際のソース取得元や、デプロイ先に直接AWSサービスを利用できることや、ログの管理もCloudWatchで手軽にできます。
ビルドやデプロイなどの各処理もパイプラインリソースごとにカスタマイズできるため、メリットが多いです。
料金もリーズナブルで、主にかかるのはCodeBuildでのビルド回数に応じた課金とCodePipelineで用意したpipelineの数に対する料金です。追加で付随するサービスの料金は別途かかります。(ビルドのログをCloudWatchに出力する場合はCloudWatchでかかる料金)
実際に計算してみました。
以下が開発サービスのモデルケースで計算した例です。
■ モデルケース
・ビルドのコンピューティングタイプはbuild.general1.small
・smallだと0.005USD/1分
・1サービスのビルド詳細
- ビルド時間 : 8分/1回
- ビルド回数合計 : 10回/1日
- パイプライン数(1USD/1本) : 4本 (develop, staging, production, etc ...)
■ 計算
【CodeBuild】
1日あたり(分) : 10回 × 8分 = 80分
1ヶ月(営業日)あたり(分) : 80 × 20 = 1600
1ヶ月あたり(USD) : 1600 × 0.005USD = 8USD
パイプラインの値段も合算すると、1サービス1ヶ月あたり 約12USD 程です。
安い!
※参考
・CodeBuildの料金
https://aws.amazon.com/jp/codebuild/pricing/
・CodePipelineの料金
https://aws.amazon.com/jp/codepipeline/pricing/
アーキテクチャ
ここからは、ECSへのビルド、デプロイの仕方でポイントを絞って書いていきます。
今回、以下の様なアーキテクチャを組みました。
開発サービスで使っているECSへのデプロイまでのフローになります。
流れとしては、GitHubの指定したブランチにコミットがあったときにWebHookが作動してCodePipelineが動きます。
CodePipelineはステージごとで処理を分けており、ソースステージで指定したリポジトリのブランチからソースを取得します。そのソースはアーティファクトとしてS3に保存され、次のビルドステージに渡されます。
ビルドステージではECSでタスク定義更新のためのコンテナイメージを作成します。Dockerfileに指定の処理を記述しておき、 docker build でイメージを作成し、docker push でECRにイメージを保存します。その後、タスク定義を作成し、作成されたタスク定義のjsonをCodeDeployに渡します。
デプロイステージでは実際にサービス更新をしています。ここではCodeDeployを用いています。
ビルド、デプロイシステムの詳細
■ WebHook
ソースステージでCodePipelineのトリガーとしてGitHubを指定する場合、以下の2つが考えられます。
・ソースステージで指定したリポジトリとブランチに対してアクションを起こす。
・明示的にWebHookを作成して、カスタマイズする。
前者ではGitHubのwebhookのアクションは指定できないため、例えばブランチの作成や削除でもパイプラインは起動してしまいますし、もし複数のソースアクションを並べた場合、セカンダリソースの参照元もトリガーになってしまいます。
後者では明示的にパイプラインとアクションを指定することで、パイプラインが起動するためのWebHookをカスタマイズ出来ます。下図のcloudformationテンプレートではブランチ指定と特定のGitHubのWebHookアクションを除外しています。
■ ソースの取得
ソースステージでは複数のソースアクションを指定できます。
開発サービスでは複数リポジトリからソースを取得してくる必要があるため、ソースステージに複数ソースアクションを追加することで実現しました。
RunOrderの値を調節することで、直列、並列取得を実現できます。下図ではRunOrderの値を等しくすることで並列にしています。
■ ビルド環境の工夫
工夫のしがいがあるステージです。
ここでは、
- ビルド時間短縮のためのキャッシュ利用
- 各ビルドコンピューティングタイプによるビルド速度の検証
- dockerイメージサイズのコンパクト化
について説明します。
[キャッシュ]
CodeBuildでローカルレイヤーキャッシュを有効にすると、Dockerfileのレイヤーがキャッシュされ、イメージ全体が毎回リビルドされるのではなく、未変更のイメージレイヤーが再利用されます。
すると、未変更のイメージレイヤーは実行されずにビルドが進むため、時間が短縮されます。
以下で効果を測定してみました。
実際に検証したところ、8分ほどかかるビルドが3分30秒ほどで終了しています。
キャッシュの有効時間はドキュメントに記載されていないですが、検証したところおおよそ1時間であると思われます。
・CodeBuildのキャッシュについて
[コンピューティングタイプ ]
CodeBuildではビルド環境に使用可能なメモリ、vCPU、ディスクベースを変更できます。
ここでは、環境タイプが LINUX_CONTAINER の以下のコンピューティングタイプによるビルドの速度を検証してみました。
・build.general1.small
・build.general1.medium
・build.general1.large
8分ほどかかるビルドがコンピューティングタイプで1分30秒ほど短縮することわかります。
ただコンピューティングタイプを上げるごとに分単位の料金が上がるため、開発サービスではビルド時間とおおよその回数から料金を計算比較し、費用対効果を考え build.general1.small を使っています。
[マルチステージビルド]
ECSインスタンスでデフォルトのAmazon Linux 2 ベースの Amazon ECS 最適化 AMIを使用している場合、30 GiB ルートボリュームが付随されており、そのうちDocker によるイメージとメタデータの保存用に 22 GiBのボリュームがアタッチされています。
大きいサイズのイメージを使いコンテナを何回も立ち上げているとボリュームを圧迫してデプロイに失敗します。そういった場合、ECSエージェントの設定でコンテナイメージのライフサイクルを早め、短時間でイメージをクリーンアップするという手もありますが、根本的な解決方法ではありません。
その際にはDockerfileでマルチステージビルドを用いるべきです。FROMを複数用いてステージを作り、Dockerfile内の各レイヤーで生成されたものをクリーンアップして、最終的に必要なレイヤーのみを利用することでイメージのサイズがコンパクトになります。
そうすることでコンテナイメージの管理がスリムになります。
上記では最終的なソースが /service に存在していればいいので、COPY、RUNが行われているレイヤーでの生成物をクリーンアップするために2つめのFROMを用いています。こうすることでイメージサイズは小さくなります。
レイヤーが多いほど効果的です。
デプロイ環境
ここではCodeDeployを使います。
以下のようなCodePipelineステージを用意するのみで、デプロイアプリケーションもデプロイメントグループも使いません。
その代わりにCodeBuildでタスク定義のjsonを作成し、アウトプットアーティファクトとしてCodeDeployに渡しています。
なぜそのような構成にしているかというと、開発サービスでは環境変数として、いくつかの変数をタスク定義にわたす必要がありました。その中には秘匿な変数も存在するため、そのような変数はビルドプロジェクトで渡すしか方法がなかったのです。
秘匿な環境変数はコードに書くことができないのでCloudformationのyamlテンプレートでDynamic Referencesという記述方法でSecret Managerから渡しています。
・Dynamic Referencesについて
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/dynamic-references.html
あとCodeDeployを使うメリットとして、ECSサービスでタスクの起動の可否についてまで確認できるということがあります。
CodeDeployの成功条件は、今回でいうとタスクが起動することです。
例えばCodeBuildでサービスの更新をするやり方だとタスクが起動したかどうかまでは確認できません。
終わりに
CodePipelineは汎用性が高く、ビルド、デプロイの詳細なシステムも自由にカスタマイズできるので、使い勝手が良かったです。
AWS Codeシリーズは頻繁にアップデートもされており、最近ではCodePipeline、CodeBuild、CodeDeployのslack通知が各サービス上で可能になったり、その通知で使われるChatbotがGAされるなど、日を追って便利になっています。
今度はその通知について書こうかなと思います。