本記事は
オブザーバビリティウィーク
5日目の記事です。
💻📄
4日目
▶▶ 本記事 ▶▶
6日目
🔔🏢
はじめに
こんにちは、藤本です。オブザーバビリティウィーク5日目を担当させていただきます。
インフラの構築や管理において、「Infrastructure as Code(IaC)」の活用が一般的になっています。 AWSにおいては、AWS CloudFormationを用いたスタック管理が代表的な手法です。 IaCを導入することで、インフラ環境をコードとしてバージョン管理できるため、再現性や一貫性が飛躍的に向上します。 しかし、CloudFormationによる自動化・管理の仕組みが整っていても、実運用の現場では意図しないリソースの設定変更によって構成逸脱(ドリフト)を引き起こす可能性があります。 たとえば、CloudFormation Stackで展開されたリソースが、以下のような要因でテンプレートと異なる状態になることがあります。
- 手動でのリソース設定変更
- スクリプトや他サービスによるリソース操作
このような状況が発生するとCloudFormation テンプレートと実際の構成が乖離し、本来あるべき状態の検証が困難になります。また、壊滅的な設定・誤った設定に変更された場合に重大なリスクを招く恐れがあります。
CloudFormation ベストプラクティスでは、スタックのリソースを CloudFormation 以外の方法で変更しないように推奨されています。
今回は、オブザーバビリティ(可観測性)の観点から、構成逸脱(ドリフト)を定期監視する仕組みについて考えてみました。
ドリフト検出とは
AWSでは、CloudFormationのドリフトを監視する仕組みとして、「ドリフト検出」が提供されています。 「ドリフト検出」とは、スタックにより管理されているリソースが、CloudFormation テンプレートの定義から逸脱していないか(ドリフトしていないか)を確認する機能です。 検出対象として、スタック全体、またはスタック内の個々のリソースのドリフトを検出します。

「ドリフト検出」が実行されると、次の流れで処理が行われます。
- テンプレートに定義された各リソースについて、現在の実リソースの構成を取得
- その構成とテンプレート定義をプロパティ単位で比較
- 差異があるかどうかを判定し、結果を検出
検出結果はリソース毎に以下の4つのステータスが表示されます。
リソースのドリフトステータス | 説明 |
---|---|
DELETED |
リソースが削除されている |
MODIFIED |
テンプレートと異なるプロパティがある |
NOT_CHECKED |
まだドリフト検出を行っていない |
IN_SYNC |
テンプレートと一致している |
ドリフト検出を使用してスタックとリソースへのアンマネージド型設定変更を検出する - AWS CloudFormation
監視構成
CloudFormationのドリフト検出をAmazon EventBridgeとAWS Lambdaで定期実行し、構成の逸脱を自動でチェックします。
ドリフト検出が完了したイベントをEventBridgeで検知し、別のLambda関数が結果を取得してAmazon SNSで通知します。
これにより、構成逸脱(ドリフト)の状況を把握し、迅速な対応が可能になります。

実装手順
実装の流れ
- EventBridgeスケジュールでLambda①を定期実行
- Lambda①でCloudFormation ドリフト検出を実行
- ドリフト検出された結果をEventBridgeルールでキャッチし、Lambda②を実行
- Lambda②でドリフト検出の結果を取得し、SNSで通知
1. EventBridgeスケジュールでLambda①を定期実行
EventBridgeスケジュールでCloudFormation ドリフト検出を実行するLambda①をInvokeするように設定します。
今回は、1時間に1回ドリフト検出を行うため、定期的なスケジュールでcron ( 00 * * * ? * )
と設定します。
また、他の設定は以下を参考にしてください。
- ターゲット : Lambda①(
StartDetectStackDrift
) - サービス : AWS Lambda
- API : Invoke
- IAMロール :
lambda:InvokeFunction
アクションを許可 - フレックスタイムウィンドウ : オフ
- Lambda①のリソースベースのポリシーステートメント(※Lambda①作成後)
- サービス : EventBridge (CloudWatch Events)
- プリンシパル : events.amazonaws.com
- ソース ARN :Lambda①(
StartDetectStackDrift
)のARN - アクション : lambda:InvokeFunction
2. Lambda①でCloudFormation ドリフト検出を実行
EventBridgeスケジュールによってCloudFormation ドリフト検出を行うLambda①(StartDetectStackDrift
)を実行します。
AWS SDKであるboto3を用いて、CloudFormation ドリフト検出を開始するdetect_stack_driftを実行します。
detect_stack_driftは、あくまでもドリフト検出を開始するだけで、検出完了までには時間がかかり、非同期に処理されます。
ドリフト検出の完了ステータスを確認したい場合は、describe_stack_drift_detection_statusのレスポンスDetectionStatus
を取得する必要があります。レスポンスDetectionStatus
には、ドリフト検出中である「DETECTION_IN_PROGRESS」、ドリフト検出が失敗した「DETECTION_FAILED」、ドリフト検出が完了した「DETECTION_COMPLETE」の3つの状態が存在します。
- Lambda①コード
import boto3 import logging import time logger = logging.getLogger() logger.setLevel(logging.INFO) cf = boto3.client('cloudformation') def lambda_handler(event, context): try: # Stackの一覧を取得 stacks = cf.list_stacks(StackStatusFilter=['CREATE_COMPLETE', 'UPDATE_COMPLETE'])['StackSummaries'] except Exception as e: logger.error(f"Failed to list stacks: {e}") return for stack in stacks: stack_name = stack['StackName'] try: # ドリフト検出を開始 detect_response = cf.detect_stack_drift(StackName=stack_name) detection_id = detect_response['StackDriftDetectionId'] logger.info(f"Started drift detection for {stack_name}: {detection_id}") except Exception as e: logger.warning(f"Drift detection not started for {stack_name}: {e}")
Lambda関数内で利用するListStacks
,DetectStackDrift
,DetectStackResourceDrift
に加えて、ドリフト検出の対象となるCloudFormation Stackによって展開されているリソースの参照権限が必要になります。
この権限が適切に設定されていないと、権限エラーによりドリフト検出が失敗した「DETECTION_FAILED」の状態となり、正しいドリフト検出を取得できません。
今回は後ほどテストの際に、参照するリソース(Amazon DynamoDB、Amazon SQS、Amazon SNS)の設定を参照できる権限をアタッチしています。
- Lambda①で利用するIAMポリシー
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "cloudformation:ListStacks", "cloudformation:DetectStackDrift", "cloudformation:DetectStackResourceDrift", "dynamodb:DescribeTable", "sqs:GetQueueAttributes", "sns:GetTopicAttributes", "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents" ], "Resource": "*" } ] }
3. ドリフト検出された結果をEventBridgeルールでキャッチし、Lambda②を実行
detect_stack_driftは、非同期に実行されるため、CloudFormation ドリフト検出が完了したら発生するイベントCloudFormation Stack Drift Detection Status
をEventBridgeルールでキャッチします。
ドリフト検出が完了したイベントのみをキャッチするために、"detection-status": ["DETECTION_COMPLETE"]
もイベントパターンに記載します。
この記載が無いと、ドリフト検出が進行中である「DETECTION_IN_PROGRESS」の状態もマッチするため、ドリフト検出結果を正しく取得できません。
ターゲットにLambda②('NotifyStackDriftViaSNS')を選択します。 Lambda①と同様に、Lambda②にもリソースベースのポリシーステートメントを設定することを忘れないようにしてください。
- イベントパターン
{ "source": ["aws.cloudformation"], "detail-type": ["CloudFormation Drift Detection Status Change"], "detail": { "status-details": { "stack-drift-status": ["DRIFTED"], "detection-status": ["DETECTION_COMPLETE"] } } }
4. Lambda②でドリフト検出の結果を取得し、SNSで通知
EventBridgeルールで直接SNSをターゲットに設定することもできますが、Lambda②(NotifyStackDriftViaSNS
)を挟むことで、SNSで通知する内容を整形することができます。
これにより、視認性を高め、ドリフトが発生しているリソースに対して迅速に対応できるようになります。
また、CloudFormationのドリフト検出イベントCloudFormation Drift Detection Status Change
では、スタック全体のドリフト状態のみの取得になります。
今回はスタック単位のドリフトの確認ではなく、リソース単位でのドリフトとその詳細について確認したいため、Lambda②(NotifyStackDriftViaSNS
)で
describe_stack_resource_driftsを利用します。
通知例は以下のようになります。
⚠️ Drift detected in production-app-stack - MyS3Bucket (AWS::S3::Bucket): MODIFIED - BucketEncryption.ServerSideEncryptionConfiguration[0].BucketKeyEnabled: expected `true`, actual `false` - MyIAMRole (AWS::IAM::Role): DELETED View in Console: https://console.aws.amazon.com/cloudformation/...
今回はテスト通知のため、SNSトピックのサブスクリプションにメールアドレスを登録しました。
このLambda②(NotifyStackDriftViaSNS
)は、ドリフト検出があったスタックごとに通知が飛ぶような仕様になっているため、実際の運用では、Slackを用いた通知のほうが管理がしやすいと思います。
また、環境変数SNS_TOPIC_ARN
からSNSトピックのARNを取得するようにしています。
- Lambda②コード
import boto3 import logging import json import urllib.parse import os logger = logging.getLogger() logger.setLevel(logging.INFO) cf = boto3.client('cloudformation') sns = boto3.client('sns') SNS_TOPIC_ARN = os.environ.get("SNS_TOPIC_ARN") def lambda_handler(event, context): logger.info("Received EventBridge event:") logger.info(json.dumps(event)) # イベントからドリフトの検出結果取得 stack_id = event["detail"]["stack-id"] stack_name = stack_id.split("/")[-2] region = event["region"] drifted_resources = [] next_token = None while True: params = { 'StackName': stack_name, 'MaxResults': 100 } if next_token: params['NextToken'] = next_token response = cf.describe_stack_resource_drifts(**params) for res in response["StackResourceDrifts"]: if res["StackResourceDriftStatus"] != "IN_SYNC": resource = { "logical_id": res["LogicalResourceId"], "resource_type": res["ResourceType"], "drift_status": res["StackResourceDriftStatus"], "property_diffs": [] } for diff in res.get("PropertyDifferences", []): resource["property_diffs"].append({ "path": diff["PropertyPath"], "expected": diff["ExpectedValue"], "actual": diff["ActualValue"], "type": diff["DifferenceType"] }) drifted_resources.append(resource) next_token = response.get("NextToken") if not next_token: break # 出力メッセージ作成 if drifted_resources: lines = [f"⚠️ Drift detected in {stack_name}"] for r in drifted_resources: lines.append(f"- {r['logical_id']} ({r['resource_type']}): {r['drift_status']}") for pd in r.get("property_diffs", []): lines.append(f" - {pd['path']}: expected `{pd['expected']}`, actual `{pd['actual']}`") # CloudFormationコンソールリンク encoded_stack_id = urllib.parse.quote(stack_id, safe='') cf_url = f"https://console.aws.amazon.com/cloudformation/home?region={region}#/stacks/stackinfo?stackId={encoded_stack_id}" lines.append("") lines.append(f"View in Console: {cf_url}") message = "\n".join(lines) logger.info(message) # SNS通知送信 sns.publish( TopicArn=SNS_TOPIC_ARN, Subject=f"Drift Detected: {stack_name}", Message=message ) else: logger.info(f"⚠️ Drift event received for {stack_name}, but no drifted resources found.")
- Lambda②で利用するIAMポリシー
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents", "cloudformation:DescribeStackResourceDrifts", "sns:Publish" ], "Resource": "*" } ] }
実際の通知を確認
ドリフトを意図的に検出させるためにテスト用に作成したCloudFormation テンプレートで定義しているリソースの設定を手動変更します。
- テスト用のテンプレート
AWSTemplateFormatVersion: '2010-09-09' Description: General purpose template to test CloudFormation drift detection Resources: DriftTestSQSQueue: Type: AWS::SQS::Queue Properties: QueueName: DriftTestQueue VisibilityTimeout: 30 DelaySeconds: 0 DriftTestSNSTopic: Type: AWS::SNS::Topic Properties: TopicName: DriftTestTopic DisplayName: Drift Test Topic DriftTestDynamoDBTable: Type: AWS::DynamoDB::Table Properties: TableName: DriftTestTable BillingMode: PAY_PER_REQUEST AttributeDefinitions: - AttributeName: id AttributeType: S KeySchema: - AttributeName: id KeyType: HASH
下記の表に従い、各リソースのプロパティを手動変更します。
リソース | 手動変更するプロパティ | 変更方法 | 期待されるリソースのドリフトステータス |
---|---|---|---|
SNS トピック | 表示名(DisplayName) | SNSコンソール > 編集から、表示名(Display name) を 「Drift Test Topic」から 「Changed Display Name"」に変更 | MODIFIED |
SQS キュー | 可視性タイムアウト(VisibilityTimeout) | SQSコンソール > 編集から、可視性タイムアウト(VisibilityTimeout)を30から45に変更 | MODIFIED |
DynamoDB | テーブル | DynamoDBコンソールから「DriftTestTable」を削除 | DELETED |
手動変更によって構成逸脱が発生している状態で、Lambda①(StartDetectStackDrift
)の定期実行を待ちます。
Lambda①(StartDetectStackDrift
)が定期実行され、ドリフト検出が完了すると、CloudFormation マネジメントコンソールにも結果が反映されます。
想定通り、手動変更したリソースに対してドリフトが検出されています。

また、メール通知も確認できました。 View in ConsoleのURLからドリフト検出があったCloudFormation マネジメントコンソールに移動することができます。

実運用のおすすめ
今回は、テスト環境での検証であったため、Lambdaが実行されるStackの対象が1つでしたが、実際に運用している環境では、複数のStackが対象となる場合が多いと思います。
そのような場合は、タグを利用することで監視対象を絞るような実装もできます。
通知の必要がない検証用Stackや、すべての環境に一律で適用しているガードレール設計に用いられるような設定のStackについては、通知の対象から除外することで、より重要な変更に集中できるようになります。
また、このようなガードレール用途のスタックについては、そもそも意図しない設定変更が行われないよう、SCP(Service Control Policy)などで変更自体を制限することも検討するとよいでしょう。
今回作成した構成逸脱(ドリフト)を監視する構成をCloudFormationでテンプレート化し、CloudFormation StackSetsによる展開で、マルチリージョンに対応することも可能です。
AWS Configルールとの違い
AWS Configルールに、ドリフト検出をチェックするcloudformation-stack-drift-detection-check
があります。
このルールでは、DetectStackDriftオペレーションを定期実行し、スタックのドリフトステータスが DRIFTED
の場合、Config ルールは NON_COMPLIANT
(非準拠)と見なします。
一方でスタックのドリフトステータスがIN_SYNC
の場合はCOMPLIANT
(準拠)となります。
準拠または非準拠の状態から、ドリフト検出の有無は確認することができますが、スタックレベルのドリフト検出となり、リソース単位の詳細は取得できません。
また、Configルールの評価タイミングは定期(1,3,6,12,24時間の中から設定) またはCloudFormationスタック変更時に実行されます。
今回のようにEventBridgeスケジュールによるドリフト検出実行であれば任意の期間を指定することができます。
Configルール自体マネージドで管理がいらず、簡単に設定ができる反面、タグベース除外やデータの整形の処理ができないなど柔軟性がないこともデメリットとして挙げることができます。
そのため、Configルールを利用する際は、監査目的での定期実行で利用するのが良いと思います。
終わりに
今回は、CloudFormation ドリフト検出機能を用いて、コードベースで構築したインフラが「いつの間にか変わっていた」問題を自動で検出する仕組みを構築してみました。
Amazon Bedrockと組み合わせて、ドリフトの検出結果から修復方法の手順を自動作成するなど、まだ改善の余地があると思います。
皆さんも、インフラの健全性を継続的に保つために、ドリフト検出を取り入れた仕組みづくりをぜひ試してみてください!