AWS Auto Scaling GroupのBlue/Greenデプロイにおける Amazon CloudWatch Alarm自動更新の実装
はじめに
コンサル部の神野です。
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アラームを自動更新する仕組みを構築します。イメージとしては下記の通りとなります。
具体的な流れは以下のとおりです。
- CodeDeployがデプロイ成功時にSNSトピックに通知
- SNSトピックがLambda関数をトリガー
- Lambda関数が古いアラームを削除し、新しいASG名でアラームを再作成
実装手順
前提
AWS CDK(以下CDK)を使用するため、CDKを事前にインストールする必要がございます。
使用したバージョンは下記となります。
cdk --version
2.1004.0 (build f0ad96e)
node -v
v20.16.0
使用するレポジトリ
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
として作成します。
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関数の主な処理フローは以下の通りです。
- SNSからのイベントを受け取り、デプロイ情報を抽出
- 新しいASGの名前を構築
- 監視対象のメトリクスタイプ(CPU、ステータスチェック、メモリ)を定義
- 各メトリクスタイプに対して:
- 既存のアラーム(同じプレフィックスを持つもの)を削除
- 新しいASG名を参照するアラームを作成
- 処理結果を返却
既存のアラームを削除して、新しいアラームを設定しなおします。
アラームの設定は別ファイルで管理し、メンテナンス性を高めています。
もし削除 & 更新したいアラームを追加したい場合は下記ファイル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トピックに通知するトリガーを設定します。
アプリケーションの作成
- AWS Management Consoleにログインし、CodeDeployサービスに移動します。
- コンソール上から
アプリケーションの作成
ボタンを押下します。
- 以下の情報を入力してアプリケーションを作成します:
- アプリケーション名:任意の名前(例:
sample-application
) - コンピューティングプラットフォーム:
EC2/オンプレミス
- アプリケーション名:任意の名前(例:
デプロイグループの作成
- アプリケーション作成後、
デプロイグループの作成
ボタンを押下します。
- デプロイグループの詳細を設定します。Blue/Greenデプロイを選択し、必要な設定を行います。
- デプロイグループ名: 任意の名前(例:
sample-dg-bluegreen
) - サービスロール: CodeDeployがAWSリソースにアクセスするためのIAMロールを選択
- デプロイタイプ:
Blue/Green
を選択 - 環境設定:
Amazon EC2 Auto Scaling グループ
を選択- CDKで作成したASGを選択
- デプロイ設定: デプロイ戦略を選択(例:
CodeDeployDefault.AllAtOnce
) - ロードバランサー: ALBとターゲットグループを設定
- デプロイグループ名: 任意の名前(例:
デプロイトリガーの設定
- デプロイグループ設定画面の下部にある
トリガー
セクションでトリガーの作成
ボタンを押下します。
- 以下の設定でデプロイトリガーを作成します:
- トリガー名:任意の名前(例:
notify-lambda-on-success
) - イベント:
デプロイが成功した場合
を選択 - Amazon SNS トピック:CDKで作成したSNSトピック (
codedeploy-deployment-notifications
) を選択
- トリガー名:任意の名前(例:
- トリガーの設定が完了したら、デプロイグループの作成を完了します。
これにより、デプロイが成功した際に自動的にSNSトピックに通知が送信され、Lambda関数が起動する仕組みが整いました。
動作確認
設定が完了したら、実際にCodeDeployを使用してBlue/Greenデプロイを実行し、自動更新の仕組みが正しく機能するか確認します。
デプロイの実行
- CodeDeployコンソールから
デプロイの作成
ボタンを押下します。
- デプロイに使用するアプリケーションリビジョンを指定します。今回はS3に保存されたデプロイパッケージを使用します。CDKの出力などから
deployment.zip
がアップロードされたS3 URLをコピーします。
- デプロイの詳細を入力し、デプロイを開始します。
- アプリケーション: 作成したアプリケーションを選択
- デプロイグループ: 作成したデプロイグループを選択
- リポジトリタイプ:
Amazon S3
- リビジョンの場所:
deployment.zip
のS3 URLを入力
- デプロイの進行状況を確認します。Blue/Greenデプロイの場合、新しい(Green)環境がプロビジョニングされ、トラフィックが切り替えられます。デプロイが成功すると、設定したトリガーによりLambda関数が起動します。
CloudWatchアラームの確認
デプロイが成功したら、CloudWatchコンソールでアラームが正しく更新されているか確認します。
- CloudWatchコンソールに移動し、ナビゲーションペインで
アラーム
>すべてのアラーム
を選択します。 - アラーム一覧で、Lambda関数によって作成された新しいアラーム(例:
ASG-HighCPUUtilization-d-XXXXXXXXX
)が存在し、新しいASG名を参照していることを確認します。ASG名でフィルターすると確認しやすいです。
対象のアラームが作成されていますね!
再デプロイによる検証
さらに検証するため、もう一度デプロイを実行し、アラームが正しく更新されるか確認します。
- 再度、CodeDeployコンソールから
デプロイの作成
を実行します。リビジョンは同じものでも、新しいものでも構いません。
- デプロイ設定は前回と同様に行います。
- デプロイが成功したことを確認します。
- CloudWatchコンソールで、前回デプロイ時に作成されたアラームが削除され、今回のデプロイに対応する新しいASG名でアラームが再作成されていることを確認します。アラーム名に含まれるデプロイIDが変わっているはずです。
無事に既存アラームが削除されて、新しいアラームが作成されていることが確認できました!これにより、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
}
// 必要に応じてディスクやネットワークなどのメトリクスも追加
}
}
}
この設定では、ApplicationName
とEnvironment
という固定ディメンションを使用し、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による自動更新アプローチとの違いになります。どちらのアプローチが適しているかは、システムの要件や運用体制に応じて検討してください。