michimani.net

節約のために CodePipeline をやめて CodeBuild に全部任せてみた

2019-08-02

CodePipeline を使うとビルド、デプロイ、結果の通知までを簡単に構築できますが、作成後 30 日以降は料金が発生してしまいます。なので、節約のために CodeBuild で全部やってしまおうという話です。

目次

前提

あくまでも個人で運用している AWS アカウントの料金節約を目的としています。また、今回やってみる内容では、 CodePipeline で実現できていることを全て再現することはできません。諦めなければいけない挙動もあるので、その点はご理解ください。

CodePipeline と CodeBuild の料金

タイトルで 節約のために と書いている通り、今回の内容で毎月の料金を節約することができるので、 CodePipeline と CodeBuild についてあらためて確認してみます。

CodePipeline

CodePipeline は、利用するパイプラインの数に対して料金が発生します。

CodeBuild

CodeBuild は、ビルドにかかった時間 (分) に対して料金が発生します。(1 分未満は切り上げ)
またその単価は、利用するコンピューティングタイプによって変わります。

タイプ メモリ (GB) vCPU Linux (USD/分) Windows (USD/分)
build.general1.small 3 2 0.005 該当なし
build.general1.medium 7 4 0.010 0.018
build.general1.large 15 8 0.020 0.036

無料利用枠として、 1 ヶ月あたりビルドを 100 分使用できます。


CodeBuild を使って該当の Vue.js アプリケーションのビルド・デプロイするのにかかる時間は 1 - 2 分程度なので、 build.general1.small を使用する場合は 1 回につき 0.02 USD 程度ということになります。
CodePipeline を使っている場合だと、プラス 1 USD (1 回きりですが) が発生します。

やっていたこと/やること

まずは CodePipeline を使っていたときの構成についてです。
イメージとしてはこんな感じです。

CodePipeline architecture
  1. GitHub の master ブランチへの Push をトリガーにパイプラインが開始
  2. CodeBuild でビルド、生成されたソースを S3 へ配置
  3. パイプラインの実行結果を Slack へ通知するための Lambda を実行

これを、次のような構成にします。

CodePipeline architecture

シンプルになりました。

構成はこれでいいとして、 CodeBuild の設定、 buildspec.yml も変更する必要があります。具体的には下記のポイントです。

では、設定の変更方法についてみていきます。

CodeBuild の設定変更

送信元を GitHub に変更

まずは送信元を GitHub に変更します。が、既に CodePipeline に紐づいているビルドプロジェクトを変更する場合、送信元はマネジメントコンソールで変更することができません。なので CLI で変更します。

一旦、既存の設定を JSON 形式で取得します。

$ aws codebuild batch-get-projects --names "BuildProjectName" > ./current.json
{
    "projects": [
        {
            "name": "BuildProjectName",
            "arn": "arn:aws:codebuild:ap-northeast-1:123456789012:project/BuildProjectName",
            "source": {
                "type": "CODEPIPELINE",
            },
            "artifacts": {
                "type": "CODEPIPELINE",
                "name": "BuildProjectName",
                "packaging": "NONE"
            },
            "cache": {
                "type": "NO_CACHE"
            },
            "environment": {
                "type": "LINUX_CONTAINER",
                "image": "aws/codebuild/nodejs:10.14.1",
                "computeType": "BUILD_GENERAL1_SMALL",
                "environmentVariables": [],
                "privilegedMode": false
            },
            "serviceRole": "arn:aws:iam::123456789012:role/service-role/codebuild-BuildProjectName-service-role",
            "timeoutInMinutes": 60,
            "encryptionKey": "arn:aws:kms:ap-northeast-1:123456789012:alias/aws/s3",
            "tags": [],
            "created": 1557990804.228,
            "lastModified": 1562114621.841,
            "badge": {
                "badgeEnabled": false
            }
        }
    ],
    "projectsNotFound": []
}

こんな感じの JSON が取得できるので、これを元にして次のような JSON ファイルを新たに作成します。(仮に update.json とします)
変更点としては、 sourceartifacts です。
environment.environmentVariables については、後の Slack 通知で使用する環境変数になります。多分 Slack の Webhook URL は Systems Manager のパラメータストアから、 KMS で暗号化した上で取得したほうがよさそうです。このへんはまだちょっとわかってないので、一旦プレーンテキストで入れてます。

{
    "name": "BuildProjectName",
    "source": {
        "type": "GITHUB",
        "location": "https://github.com/michimani/repo.git"
    },
    "artifacts": {
        "type": "NO_ARTIFACTS"
    },
    "environment": {
        "type": "LINUX_CONTAINER",
        "image": "aws/codebuild/standard:2.0",
        "computeType": "BUILD_GENERAL1_SMALL",
        "environmentVariables": [
            {
                "name": "notifyTitle",
                "value": "BuildProjectName build",
                "type": "PLAINTEXT"
            },
            {
                "name": "slackHookUrl",
                "value": "https://hooks.slack.com/services/123456789/ABCDEFGHI/abcdefghij1234567890abcd",
                "type": "PLAINTEXT"
            }
        ],
        "privilegedMode": false
    }
}

この JSON ファイルを使って CodeBuild の設定を変更します。

$ aws codebuild update-project --cli-input-json file://update.json

正しく変更できれば、変更後の設定が出力されます。

プライマリソースのウェブフックイベントを設定

続いてはビルド開始の設定です。これについてはマネジメントコンソールで操作しました。

CodeBuild のビルドプロジェクトから対象のプロジェクトを選択して、送信元の編集画面を開きます。
ちなみに、この時点でソースプロバイダが GitHub になっていることを確認できます。

CodeBuild resource setting

画面の下の方にある プライマリソースのウェブフックイベント で、ビルド開始条件を設定します。

CodeBuild resource webhook event

イベントタイプ で プッシュ を選択し、 これらの条件でビルドを開始するHEAD_REF^refs/heads/master$ と入力して更新します。
この master 部分を変更することで、特定のブランチへのイベントを検知することができます。

buildspec.yml 内で Slack への通知を追加

最後に、 Slack への通知処理を buildspec.yml 内に追加します。処理といっても、下記の curl コマンドを post_build フェーズの commands に追加するだけです。

curl -H 'Content-Type:application/json' -d "{'channel':'dev','icon_emoji':':codebuild:','attachments':[{'author_name':'CodeBuild','title':'$notifyTitle','color':'#00FF00','fields':[{'value':'Build and deploy were successed.'}]}]}" $slackHookUrl

POST している JSON は次のような構造になっています。

{
    "channel": "dev",
    "icon_emoji": ":codebuild:",
    "attachments": [
        {
            "author_name": "CodeBuild",
            "title": "$notifyTitle",
            "color": "#00FF00",
            "fields": [
                {
                    "value": "Build and deploy were successed."
                }
            ]
        }
    ]
}

実際に通知されるとこんな感じになります。

Slack Notifir

以上で、 CodeBuild のみでビルド・デプロイ・通知までできるようになりました。

失ったもの ※追記あり (2019/11/12)

CodePipeline を使用しなくなったことで料金の節約には成功しましたが、 CI/CD のクオリティとしては下がっています。
特に問題なのが、 ビルドに失敗したことがわからない という点です。

CodePipeline を使っていた時は、仮に CodeBuild の処理中にエラーが発生した場合はあパイプラインの失敗となります。失敗したというステータスは Slack 通知用の Lambda 内で取得できるので、成功しても失敗しても、どちらの場合も通知で確認することができます。

一方、今回のように CodeBuild だけでやろうとすると、 CodeBuild 内でのエラー・失敗をキャッチして通知する術がありません。
本当に無理やりやろうとするなら、 buildspec.yml 内の各フェーズの最初に通知コマンドを実行して、最後のフェーズの通知が来なかったらその前段階で失敗したということはわかるかもしれません。


※追記 2019/11/12
AWS の Code シリーズ向けに新たな通知サービスがリリースされ、各 Code シリーズから個別に SNS および AWS Chatbot (beta) にイベント通知を送信できるようになりました。
その結果、失ったと思われたものはクオリティが上がって戻ってきました!詳細は下記のブログで書いてます。

まとめ

節約のために CodePipeline をやめて CodeBuild に全部任せてみたという内容でした。
失ったもの で書いたように、 CI/CD のクオリティとしてはだいぶ低くなります。GitHub への Push でビルド・デプロイしたい!通知はとりあえず何かくれば OK! くらいのゆるーい感じであれば、 CodeBuild だけでもなんとかなりそうです。

そもそも他の CI/CD サービス使えばいいのかもしれませんが…。

以上、よっしー (michimani) でした。