AWS Auto Scaling GroupのBlue/Greenデプロイにおける Amazon CloudWatch Alarm自動更新の実装

AWS Auto Scaling GroupのBlue/Greenデプロイにおける Amazon CloudWatch Alarm自動更新の実装

Clock Icon2025.04.30

はじめに

コンサル部の神野です。

AWSでのデプロイ自動化で、AWS CodeDeploy(以下CodeDeploy)を使用したAuto Scaling Group(以下ASG)のBlue/Greenデプロイは非常に便利な機能です。しかし、Blue/Greenデプロイを実施すると新しいASGが作成され、古いASGが削除されるため、ASG名をディメンションとしていたメトリクスの場合は古いASGに紐づいたAmazon CloudWatch Alarm(以下CloudWatchアラーム)が意味をなさなくなるといった問題があります。

今回はこの問題に対する解決策として、CodeDeployのデプロイ成功時にAWS Lambda(以下Lambda)関数を起動し、CloudWatchアラームを自動的に更新する方法をご紹介します。

問題の背景

Blue/Greenデプロイでは、新環境(Green)を作成し、テスト後に本番環境(Blue)と切り替えるという流れで実施されます。この際、新しいASGが作成され、古いASGは削除されます。

CloudWatchアラームはディメンションとしてASG名を指定することで、特定のASGのメトリクス(CPU使用率やメモリ使用率など)を監視しているケースがあります。しかし、Blue/Greenデプロイによって古いASGが削除され新しいASGが作成されると、既存のアラームは古いASG名を参照したままとなり、有効なモニタリングができなくなります。

解決策の概要

この問題を解決するために、CodeDeployのデプロイ成功イベントをトリガーとして、Lambda関数でCloudWatchアラームを自動更新する仕組みを構築します。イメージとしては下記の通りとなります。

CleanShot 2025-04-30 at 01.01.07@2x

具体的な流れは以下のとおりです。

  1. CodeDeployがデプロイ成功時にSNSトピックに通知
  2. SNSトピックがLambda関数をトリガー
  3. Lambda関数が古いアラームを削除し、新しいASG名でアラームを再作成

実装手順

前提

AWS CDK(以下CDK)を使用するため、CDKを事前にインストールする必要がございます。
使用したバージョンは下記となります。

使用したバージョン
cdk --version 
2.1004.0 (build f0ad96e)

node -v
v20.16.0

使用するレポジトリ

https://github.com/yuu551/asg-alarm-auto-update

1. CDKによるインフラストラクチャのデプロイ

CDKを使用して、必要なリソースを効率的にデプロイします。CodeDeployの設定以外の部分はCDKで自動化することで、環境構築の再現性を高めています。(再現のための環境となります)

以下のコマンドでリポジトリをクローンし、CDKを実行します。

実行コマンド
# リポジトリをクローン
git clone https://github.com/yuu551/asg-alarm-auto-update

# ディレクトリ移動
cd asg-alarm-auto-update

# 依存モジュールをインストール
npm install

# デプロイ実行
cdk deploy

CDKでは主に以下のリソースを作成します。

  • VPCとサブネット(パブリック/プライベート)
  • Application Load Balancer(ALB)とセキュリティグループ
  • Auto Scaling Group(ASG)とEC2インスタンス
  • CodeDeployのデプロイ用S3バケット
  • SNSトピック(codedeploy-deployment-notifications)
  • Lambda関数(CloudWatchアラーム更新用)とIAMロール
    • CloudWatchアラームの作成/削除権限
    • SNSトピックへの発行権限
    • CodeDeployとAuto Scalingの参照権限
  • CloudWatchアラーム
    • CPU使用率監視(70%超過)
    • ステータスチェック監視
    • メモリ使用率監視(70%超過)

Lambda 関数の実装詳細

今回の解決策の中核となるLambda関数について、詳しく見ていきましょう。この関数は、デプロイ成功時にSNSから通知を受け取り、CloudWatchアラームを更新します。
下記ファイルをupdateAlarmHandler.jsとして作成します。

updateAlarmHandler.js
import { CloudWatch } from '@aws-sdk/client-cloudwatch';
import { ALARM_CONFIGS } from './alarmConfigs.js';

const cloudwatch = new CloudWatch({ region: 'ap-northeast-1' });

// 再帰的にアラームを取得する関数
async function getAllAlarms(params, allAlarms = []) {
    const response = await cloudwatch.describeAlarms(params);

    const updatedAlarms = response.MetricAlarms
        ? [...allAlarms, ...response.MetricAlarms]
        : allAlarms;

    if (response.NextToken) {
        return getAllAlarms(
            { ...params, NextToken: response.NextToken },
            updatedAlarms
        );
    }

    return updatedAlarms;
}

// アラームを作成する関数
async function createMetricAlarm(metricType, deploymentId, autoScalingGroupName) {
    if (!ALARM_CONFIGS[metricType]) {
        throw new Error(`未定義のメトリクスタイプ: ${metricType}`);
    }

    const config = ALARM_CONFIGS[metricType];

    const params = {
        AlarmName: `${config.prefix}-${deploymentId}`,
        AlarmDescription: config.description, 
        MetricName: config.metricName,
        Namespace: config.namespace,
        Statistic: config.statistic,
        Period: config.period,
        Threshold: config.threshold,
        ComparisonOperator: config.comparisonOperator,
        EvaluationPeriods: config.evaluationPeriods,
        Dimensions: [{
            Name: 'AutoScalingGroupName',
            Value: autoScalingGroupName
        }],
        // 直接記載するのではなく、環境変数などから取得するほうがベター
        AlarmActions: ['arn:aws:sns:ap-northeast-1:xxx:anomaly_detection']
    };

    return cloudwatch.putMetricAlarm(params);
}

// 既存のアラームを削除する関数
async function deleteExistingAlarms(metricType, deploymentId) {
    if (!ALARM_CONFIGS[metricType]) {
        throw new Error(`未定義のメトリクスタイプ: ${metricType}`);
    }

    const config = ALARM_CONFIGS[metricType];
    const prefix = `${config.prefix}-`;

    try {
        const listAlarmsParams = {
            AlarmNamePrefix: prefix,
            MaxRecords: 100
        };

        const allAlarms = await getAllAlarms(listAlarmsParams);

        // 削除対象のアラーム名をフィルタリング
        const alarmsToDelete = allAlarms
            .filter(alarm =>
                alarm.AlarmName.startsWith(prefix) &&
                alarm.Dimensions.some(dim =>
                    dim.Name === 'AutoScalingGroupName'
                )
            )
            .map(alarm => alarm.AlarmName);

        if (alarmsToDelete.length > 0) {
            await cloudwatch.deleteAlarms({
                AlarmNames: alarmsToDelete
            });
            console.log(`削除された既存の${metricType}アラーム:`, alarmsToDelete);
        } else {
            console.log(`削除対象の既存${metricType}アラームは見つかりませんでした。`);
        }
    } catch (deleteError) {
        // エラーが発生しても処理を継続できるように警告ログを出力
        console.warn(`既存の${metricType}アラーム削除中のエラー:`, deleteError);
    }
}

export const handler = async (event) => {
    try {
        console.log('受信イベント:', JSON.stringify(event, null, 2));

        // SNSメッセージからデプロイメント情報を取得
        const snsMessage = JSON.parse(event.Records[0].Sns.Message);

        // デプロイメント情報を取得
        const deploymentId = snsMessage.deploymentId;
        const applicationName = snsMessage.applicationName;
        const deploymentGroupName = snsMessage.deploymentGroupName;

        // 新しいAuto Scaling Group名を構築 (CodeDeployの命名規則に依存)
        const autoScalingGroupName = `CodeDeploy_${deploymentGroupName}_${deploymentId}`;

        // 処理対象のメトリクスタイプを設定
        const metricTypes = ['CPU', 'StatusCheck', 'Memory'];

        const results = {};

        // 各メトリクスタイプに対してアラームを更新
        for (const metricType of metricTypes) {
            // 既存のアラームを削除 (新しいデプロイIDに紐づかない古いアラームを削除)
            // 注意: deploymentIdは毎回変わるため、プレフィックスのみで検索し、ASGディメンションを持つものを削除
            await deleteExistingAlarms(metricType, deploymentId); // deploymentIdは新しいアラーム名生成に使うが、削除ロジックではプレフィックスで検索

            // 新しいアラームを作成
            const result = await createMetricAlarm(metricType, deploymentId, autoScalingGroupName);
            console.log(`新しい${metricType}アラームが正常に作成されました:`, result);

            results[metricType] = {
                alarmName: `${ALARM_CONFIGS[metricType].prefix}-${deploymentId}`,
                status: 'created'
            };
        }

        return {
            statusCode: 200,
            body: JSON.stringify({
                message: 'CloudWatch アラームが正常に作成/更新されました',
                autoScalingGroupName: autoScalingGroupName,
                alarms: results
            })
        };

    } catch (error) {
        console.error('エラー:', error);
        // エラー発生時もLambda実行自体は成功として扱うか、エラーを返すかは要件による
        // ここではエラーをスローしてLambda実行失敗とする
        throw error;
    }
};

Lambda関数の主な処理フローは以下の通りです。

  1. SNSからのイベントを受け取り、デプロイ情報を抽出
  2. 新しいASGの名前を構築
  3. 監視対象のメトリクスタイプ(CPU、ステータスチェック、メモリ)を定義
  4. 各メトリクスタイプに対して:
    • 既存のアラーム(同じプレフィックスを持つもの)を削除
    • 新しいASG名を参照するアラームを作成
  5. 処理結果を返却

既存のアラームを削除して、新しいアラームを設定しなおします。

アラームの設定は別ファイルで管理し、メンテナンス性を高めています。
もし削除 & 更新したいアラームを追加したい場合は下記ファイルalarmConfigs.jsを更新します。

alarmConfigs.js
// アラーム設定を定義
export const ALARM_CONFIGS = {
    CPU: {
        prefix: 'ASG-HighCPUUtilization',
        description: 'アラーム:CPU使用率が70%を超過した場合', // 日本語に統一
        metricName: 'CPUUtilization',
        namespace: 'AWS/EC2',
        statistic: 'Average',
        period: 300,
        threshold: 70,
        comparisonOperator: 'GreaterThanThreshold',
        evaluationPeriods: 2
    },
    StatusCheck: {
        prefix: 'ASG-StatusCheckFailed',
        description: 'アラーム:ステータスチェック失敗(インスタンス)', // 日本語に統一
        metricName: 'StatusCheckFailed_Instance', // EC2インスタンスのステータスチェックを想定
        namespace: 'AWS/EC2',
        statistic: 'Maximum',
        period: 300,
        threshold: 1, // 1回でも失敗したらアラーム
        comparisonOperator: 'GreaterThanOrEqualToThreshold',
        evaluationPeriods: 1 // 1期間で評価
    },
    Memory: {
        prefix: 'ASG-HighMemoryUtilization',
        description: 'アラーム:メモリ使用率が70%を超過した場合', // 日本語に統一
        metricName: 'mem_used_percent',
        namespace: 'CWAgent', // CloudWatch Agentで収集するメトリクスを想定
        statistic: 'Average',
        period: 300,
        threshold: 70,
        comparisonOperator: 'GreaterThanThreshold',
        evaluationPeriods: 2
    }
};

この設定ファイルにより、監視対象のメトリクスごとに異なるアラーム設定を柔軟に定義できます。新しいメトリクスを追加する場合も、このファイルに設定を追加するだけで対応可能です。

2. CodeDeployデプロイグループの設定

次に、AWS Management Consoleを使用してCodeDeployの設定を行います。ここでは、デプロイグループを作成し、デプロイ成功時にSNSトピックに通知するトリガーを設定します。

アプリケーションの作成

  1. AWS Management Consoleにログインし、CodeDeployサービスに移動します。
  2. コンソール上からアプリケーションの作成ボタンを押下します。
    CleanShot 2025-04-30 at 01.03.45@2x
  3. 以下の情報を入力してアプリケーションを作成します:
    • アプリケーション名:任意の名前(例:sample-application
    • コンピューティングプラットフォーム:EC2/オンプレミス
      CleanShot 2025-04-30 at 01.04.15@2x

デプロイグループの作成

  1. アプリケーション作成後、デプロイグループの作成ボタンを押下します。
    CleanShot 2025-04-30 at 01.04.51@2x
  2. デプロイグループの詳細を設定します。Blue/Greenデプロイを選択し、必要な設定を行います。
    CleanShot 2025-03-16 at 23.32.20@2x
    • デプロイグループ名: 任意の名前(例: sample-dg-bluegreen
    • サービスロール: CodeDeployがAWSリソースにアクセスするためのIAMロールを選択
    • デプロイタイプ: Blue/Green を選択
    • 環境設定:
      • Amazon EC2 Auto Scaling グループ を選択
      • CDKで作成したASGを選択
    • デプロイ設定: デプロイ戦略を選択(例: CodeDeployDefault.AllAtOnce
    • ロードバランサー: ALBとターゲットグループを設定

デプロイトリガーの設定

  1. デプロイグループ設定画面の下部にあるトリガーセクションでトリガーの作成ボタンを押下します。
    CleanShot 2025-04-30 at 01.06.08@2x
  2. 以下の設定でデプロイトリガーを作成します:
    • トリガー名:任意の名前(例: notify-lambda-on-success
    • イベント:デプロイが成功した場合 を選択
    • Amazon SNS トピック:CDKで作成したSNSトピック (codedeploy-deployment-notifications) を選択
      CleanShot 2025-04-30 at 01.06.41@2x
  3. トリガーの設定が完了したら、デプロイグループの作成を完了します。

これにより、デプロイが成功した際に自動的にSNSトピックに通知が送信され、Lambda関数が起動する仕組みが整いました。

動作確認

設定が完了したら、実際にCodeDeployを使用してBlue/Greenデプロイを実行し、自動更新の仕組みが正しく機能するか確認します。

デプロイの実行

  1. CodeDeployコンソールからデプロイの作成ボタンを押下します。
    CleanShot 2025-04-30 at 01.07.10@2x
  2. デプロイに使用するアプリケーションリビジョンを指定します。今回はS3に保存されたデプロイパッケージを使用します。CDKの出力などからdeployment.zipがアップロードされたS3 URLをコピーします。
    CleanShot 2025-04-30 at 01.07.42@2x
  3. デプロイの詳細を入力し、デプロイを開始します。
    • アプリケーション: 作成したアプリケーションを選択
    • デプロイグループ: 作成したデプロイグループを選択
    • リポジトリタイプ: Amazon S3
    • リビジョンの場所: deployment.zip のS3 URLを入力
      CleanShot 2025-04-30 at 01.08.07@2x
  4. デプロイの進行状況を確認します。Blue/Greenデプロイの場合、新しい(Green)環境がプロビジョニングされ、トラフィックが切り替えられます。デプロイが成功すると、設定したトリガーによりLambda関数が起動します。
    CleanShot 2025-04-30 at 01.08.35@2x

CloudWatchアラームの確認

デプロイが成功したら、CloudWatchコンソールでアラームが正しく更新されているか確認します。

  1. CloudWatchコンソールに移動し、ナビゲーションペインでアラーム > すべてのアラームを選択します。
  2. アラーム一覧で、Lambda関数によって作成された新しいアラーム(例: ASG-HighCPUUtilization-d-XXXXXXXXX)が存在し、新しいASG名を参照していることを確認します。ASG名でフィルターすると確認しやすいです。
    CleanShot 2025-04-30 at 01.09.11@2x

対象のアラームが作成されていますね!

再デプロイによる検証

さらに検証するため、もう一度デプロイを実行し、アラームが正しく更新されるか確認します。

  1. 再度、CodeDeployコンソールからデプロイの作成を実行します。リビジョンは同じものでも、新しいものでも構いません。
    CleanShot 2025-04-30 at 01.09.39@2x
  2. デプロイ設定は前回と同様に行います。
    CleanShot 2025-04-30 at 01.10.03@2x
  3. デプロイが成功したことを確認します。
    CleanShot 2025-04-30 at 01.10.29@2x
  4. CloudWatchコンソールで、前回デプロイ時に作成されたアラームが削除され、今回のデプロイに対応する新しいASG名でアラームが再作成されていることを確認します。アラーム名に含まれるデプロイIDが変わっているはずです。
    CleanShot 2025-04-30 at 01.11.02@2x

無事に既存アラームが削除されて、新しいアラームが作成されていることが確認できました!これにより、Blue/Greenデプロイを実施しても、CloudWatchアラームが途切れることなく機能し続けることが検証できました。

おわりに

CodeDeployのBlue/GreenデプロイにおけるCloudWatchアラームの自動更新はいかがだったでしょうか。
Lambda関数とSNSトリガーを利用することで、デプロイプロセスに連動したアラーム管理を実現できます。少しトリッキーなやり方なような気もしますが、そこまで難しくなく実装できたかと思います。

本記事が少しでも参考になりましたら幸いです!最後までご覧いただきありがとうございました!

補足:CloudWatch Agentによる代替アプローチ

Blue/Greenデプロイにおけるアラーム管理の別の方法として、CloudWatch Agentを活用した独自メトリクス・ディメンション設計があります。この方法では、ASG名に依存しない固定ディメンションを使用することで、デプロイ後もアラームを自動更新せずに継続的な監視が可能になります。

CloudWatch Agent設定例

EC2インスタンスにインストールされたCloudWatch Agentの設定ファイル (config.json など) で、カスタムディメンションを指定します。

{
  "metrics": {
    "namespace": "CustomApplicationMetrics", // 任意の名前空間
    "append_dimensions": {
      "ApplicationName": "my-web-app", // アプリケーションを識別する固定のディメンション
      "Environment": "production",     // 環境を識別する固定のディメンション
      "AutoScalingGroupName": "${aws:AutoScalingGroupName}", // 参考情報としてASG名も追加可能
      "InstanceId": "${aws:InstanceId}"
    },
    "metrics_collected": {
      "mem": {
        "measurement": [
          {"name": "mem_used_percent", "unit": "Percent"}
        ],
        "metrics_collection_interval": 60
      },
      "cpu": { // 標準メトリクスもAgent経由で収集する場合
        "measurement": [
          {"name": "cpu_usage_idle", "unit": "Percent"},
          {"name": "cpu_usage_iowait", "unit": "Percent"},
          {"name": "cpu_usage_user", "unit": "Percent"},
          {"name": "cpu_usage_system", "unit": "Percent"}
        ],
        "totalcpu": true, // 全CPUコアの平均値
        "metrics_collection_interval": 60
      }
      // 必要に応じてディスクやネットワークなどのメトリクスも追加
    }
  }
}

この設定では、ApplicationNameEnvironmentという固定ディメンションを使用し、ASG名が変わっても同じディメンションでメトリクスを収集します。

アラーム設定例

CloudWatchアラームを作成する際に、ASG名ではなく、下記のような固定ディメンションを指定します。

// AWS CLIでの作成例
aws cloudwatch put-metric-alarm \
    --alarm-name "High-CPU-Usage-MyApp-Production" \
    --alarm-description "Alarm" \
    --metric-name "cpu_usage_system" \
    --namespace "CustomApplicationMetrics" \
    --statistic Average \
    --period 300 \
    --threshold 70 \
    --comparison-operator GreaterThanThreshold \
    --dimensions Name=ApplicationName,Value=my-web-app Name=Environment,Value=production \
    --evaluation-periods 2 \
    --alarm-actions arn:aws:sns:ap-northeast-1:ACCOUNT_ID:your-sns-topic

このアプローチのメリットは、デプロイごとにアラームを再作成する必要がなく、継続的な監視が可能になる点です。また、アプリケーションや環境といった、よりビジネスロジックに近い単位での監視が可能になります。

一方で、CloudWatch Agentの導入と設定管理が必要になります。特に、CPU使用率のような標準メトリクスもAgent経由で収集・管理する必要が出てくる点が、本記事で紹介したLambdaによる自動更新アプローチとの違いになります。どちらのアプローチが適しているかは、システムの要件や運用体制に応じて検討してください。

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.