定期的にドリフトをチェックしSNSへ通知してみた
こんにちは!AWS事業本部コンサルティング部のたかくに(@takakuni_)です。
今回は、定期的に CloudFormation スタックのドリフトを確認し、ドリフトが検出された時に SNS で通知する仕組みを作ってみようと思います。
スタックのドリフトとは
CloudFormation スタックのドリフトとは、スタックでデプロイしたリソースに対して、手動(時には自動)で設定値が変更されていないかを確認する機能です。
注意点として CloudFormation のドリフト検出は「コードとして記載されている設定値」と「現在の設定値」の差分を確認する機能のため、コードとして記載されていない部分(暗黙的なデフォルト値)については、検出されない仕様となっています。
そのため、ドリフト検出させたい部分は明示的に記載する必要があります。詳しくはこちらもご覧ください。
今回の構成
今回の構成図は以下の通りです。
AWS Config のマネージドルールで提供されている「CLOUDFORMATION_STACK_DRIFT_DETECTION_CHECK
」で定期的にスタックのドリフトを評価します。
Config ルールが「NON_COMPLIANT
」に変わったイベントをトリガーに EventBridge Rule を発火させます。
Amazon SNS を利用して登録したサブスクリプション宛に結果を通知します。
作成したコード
今回作成したコードは以下の通りです。
※ 少し長いので折りたたんでいます。クリックして適宜お使いください。
drift_detector.yaml
AWSTemplateFormatVersion: "2010-09-09"
Description: "AWS CloudFormation Stack Drift Detector"
Metadata:
AWS::CloudFormation::Interface:
ParameterGroups:
- Label:
default: "General Configuration"
Parameters:
- SystemName
- Environment
- Label:
default: "AWS Config Configuration"
Parameters:
- MaximumExecutionFrequency
- Label:
default: "Amazon SNS Configuration"
Parameters:
- EmailAddress
Parameters:
# General Configuration
SystemName:
Type: String
Description: "System Name"
Environment:
Description: "Environment Name"
Type: String
AllowedValues:
- "prd"
- "stg"
- "dev"
# AWS Config Configuration
MaximumExecutionFrequency:
Type: String
Description: "The maximum frequency with which AWS Config runs evaluations for a rule."
AllowedValues:
- "One_Hour"
- "Three_Hours"
- "Six_Hours"
- "Twelve_Hours"
- "TwentyFour_Hours"
Default: TwentyFour_Hours
# Amazon SNS Configuration
EmailAddress:
Type: String
Description: "Email Address for receiving notification"
Resources:
###################################
# AWS Config Configuration
###################################
ConfigCfnDriftPolicy:
Type: AWS::IAM::ManagedPolicy
Properties:
ManagedPolicyName: !Sub "${SystemName}-${Environment}-config-cfn-drift-policy"
Description: "IAM Policy for AWS CloudFormation Stack Drift Detector"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Action:
- "cloudformation:DetectStackResourceDrift"
- "cloudformation:DescribeStackDriftDetectionStatus"
- "cloudformation:DetectStackDrift"
Resource: "*"
ConfigCfnDriftRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub "${SystemName}-${Environment}-config-cfn-drift-role"
Description: "IAM Role for AWS CloudFormation Stack Drift Detector"
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Principal:
Service:
- "config.amazonaws.com"
Action:
- "sts:AssumeRole"
ManagedPolicyArns:
- "arn:aws:iam::aws:policy/ReadOnlyAccess"
- !Ref ConfigCfnDriftPolicy
ConfigCfnDriftRule:
Type: AWS::Config::ConfigRule
Properties:
ConfigRuleName: !Sub "${SystemName}-${Environment}-config-cfn-drift-rule"
InputParameters:
cloudformationRoleArn: !GetAtt ConfigCfnDriftRole.Arn
Scope:
ComplianceResourceTypes:
- "AWS::CloudFormation::Stack"
Source:
Owner: "AWS"
SourceIdentifier: "CLOUDFORMATION_STACK_DRIFT_DETECTION_CHECK"
MaximumExecutionFrequency: !Ref MaximumExecutionFrequency
###################################
# Amazon SNS Configuration
###################################
SnsCfnDriftTopic:
Type: AWS::SNS::Topic
Properties:
DisplayName: !Sub "${SystemName}-${Environment}-cfn-drift-info-topic"
TopicName: !Sub "${SystemName}-${Environment}-cfn-drift-info-topic"
CfnDriftSubscription:
Type: AWS::SNS::Subscription
Properties:
Endpoint: !Ref EmailAddress
TopicArn: !Ref SnsCfnDriftTopic
Protocol: "email"
SnsCfnDriftTopicPolicy:
Type: AWS::SNS::TopicPolicy
Properties:
Topics:
- !Ref SnsCfnDriftTopic
PolicyDocument:
Statement:
- Effect: "Allow"
Principal:
Service: "events.amazonaws.com"
Action: "sns:Publish"
Resource: !Ref SnsCfnDriftTopic
###################################
# Amazon EventBridge Configuration
###################################
EventsCfnDriftRule:
Type: AWS::Events::Rule
Properties:
Name: !Sub "${SystemName}-${Environment}-cfn-drift-event-rule"
EventPattern:
source:
- "aws.config"
detail-type:
- "Config Rules Compliance Change"
detail:
messageType:
- "ComplianceChangeNotification"
newEvaluationResult:
complianceType:
- "NON_COMPLIANT"
configRuleName:
- !Ref ConfigCfnDriftRule
Targets:
- Arn: !Ref SnsCfnDriftTopic
Id: "cfn-drift"
InputTransformer:
InputPathsMap:
"account": "$.detail.awsAccountId"
"region": "$.detail.awsRegion"
"resourceId": "$.detail.resourceId"
"time": "$.detail.newEvaluationResult.resultRecordedTime"
InputTemplate: |
"CloudFormation Stack のドリフトを検出しました。"
"Account ID : <account>"
"Region : <region>"
"ResourceId : <resourceId>"
"Time : <time>"
ポイントを整理
今回、作成した CloudFormation コードを実行すれば、リソースは作成できるため、はまりどこりやカスタマイズポイントをご紹介できればと思います。
AWS Config
Config ルールについて
まずは、AWS Config から解説します。
再掲になりますが、 AWS Config のマネージドルールで提供されている 「CLOUDFORMATION_STACK_DRIFT_DETECTION_CHECK
」 は、 CloudFormation スタックのドリフト検出を行ってくれるルールです。
検出タイミングは、「定期的(最大24時間から最短1時間の間隔)」と「CloudFormation スタックの変更、削除」の両方をトリガーに行われます。
今回は、「MaximumExecutionFrequency
」パラメーターを設定し、実行間隔を設定できるように設定しました。
##############(省略)################
Parameters:
# AWS Config Configuration
MaximumExecutionFrequency:
Type: String
Description: "The maximum frequency with which AWS Config runs evaluations for a rule."
AllowedValues:
- "One_Hour"
- "Three_Hours"
- "Six_Hours"
- "Twelve_Hours"
- "TwentyFour_Hours"
Default: TwentyFour_Hours
##############(省略)################
Resources:
###################################
# AWS Config Configuration
###################################
ConfigCfnDriftRule:
Type: AWS::Config::ConfigRule
Properties:
ConfigRuleName: !Sub "${SystemName}-${Environment}-config-cfn-drift-rule"
InputParameters:
cloudformationRoleArn: !GetAtt ConfigCfnDriftRole.Arn
Scope:
ComplianceResourceTypes:
- "AWS::CloudFormation::Stack"
Source:
Owner: "AWS"
SourceIdentifier: "CLOUDFORMATION_STACK_DRIFT_DETECTION_CHECK"
MaximumExecutionFrequency: !Ref MaximumExecutionFrequency
Config ルールの権限について
ドリフト検出を行うためには、以下の権限が必要です。
- ReadOnlyAccess
- DetectStackDrift API と DetectStackResourceDrift API を実行する権限
- cloudformation:DetectStackDrift
- cloudformation:DetectStackResourceDrift
- cloudformation:BatchDescribeTypeConfigurations
よって、次の IAM ロール、 IAM ポリシーを定義しました。
ConfigCfnDriftPolicy:
Type: AWS::IAM::ManagedPolicy
Properties:
ManagedPolicyName: !Sub "${SystemName}-${Environment}-config-cfn-drift-policy"
Description: "IAM Policy for AWS CloudFormation Stack Drift Detector"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Action:
- "cloudformation:DetectStackResourceDrift"
- "cloudformation:DescribeStackDriftDetectionStatus"
- "cloudformation:DetectStackDrift"
Resource: "*"
ConfigCfnDriftRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub "${SystemName}-${Environment}-config-cfn-drift-role"
Description: "IAM Role for AWS CloudFormation Stack Drift Detector"
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Principal:
Service:
- "config.amazonaws.com"
Action:
- "sts:AssumeRole"
ManagedPolicyArns:
- "arn:aws:iam::aws:policy/ReadOnlyAccess"
- !Ref ConfigCfnDriftPolicy
EventBridge
入力トランスフォーマーについて
通知内容をわかりやすくするために、 EventBridge 側で入力トランスフォーマーを利用しました。
参考までにどのようなデータ形式だったのかを添付します。
{
"version": "0",
"id": "ef91686e-52fb-fe37-048c-1821f1d1407b",
"detail-type": "Config Rules Compliance Change",
"source": "aws.config",
"account": "111111111111",
"time": "2023-02-18T05:01:51Z",
"region": "ap-northeast-1",
"resources": [],
"detail": {
"resourceId": "arn:aws:cloudformation:ap-northeast-1:111111111111:stack/drift-sample/d70616c0-af48-11ed-9429-06fa5fb981a5",
"awsRegion": "ap-northeast-1",
"awsAccountId": "111111111111",
"configRuleName": "takakuni-prd-config-cfn-drift-rule",
"recordVersion": "1.0",
"configRuleARN": "arn:aws:config:ap-northeast-1:111111111111:config-rule/config-rule-mfrooi",
"messageType": "ComplianceChangeNotification",
"newEvaluationResult": {
"evaluationResultIdentifier": {
"evaluationResultQualifier": {
"configRuleName": "takakuni-prd-config-cfn-drift-rule",
"resourceType": "AWS::CloudFormation::Stack",
"resourceId": "arn:aws:cloudformation:ap-northeast-1:111111111111:stack/drift-sample/d70616c0-af48-11ed-9429-06fa5fb981a5",
"evaluationMode": "DETECTIVE"
},
"orderingTimestamp": "2023-02-18T05:01:34.960Z"
},
"complianceType": "NON_COMPLIANT",
"resultRecordedTime": "2023-02-18T05:01:50.814Z",
"configRuleInvokedTime": "2023-02-18T05:01:50.553Z"
},
"oldEvaluationResult": {
"evaluationResultIdentifier": {
"evaluationResultQualifier": {
"configRuleName": "takakuni-prd-config-cfn-drift-rule",
"resourceType": "AWS::CloudFormation::Stack",
"resourceId": "arn:aws:cloudformation:ap-northeast-1:111111111111:stack/drift-sample/d70616c0-af48-11ed-9429-06fa5fb981a5",
"evaluationMode": "DETECTIVE"
},
"orderingTimestamp": "2023-02-18T04:59:32.186Z"
},
"complianceType": "COMPLIANT",
"resultRecordedTime": "2023-02-18T05:01:29.352Z",
"configRuleInvokedTime": "2023-02-18T05:01:29.184Z"
},
"notificationCreationTime": "2023-02-18T05:01:51.597Z",
"resourceType": "AWS::CloudFormation::Stack"
}
}
上記のデータ形式を入力トランスフォーマーを利用して、以下の形式で通知されるように変換しました。
{
"account": "$.detail.awsAccountId",
"region": "$.detail.awsRegion",
"resourceId": "$.detail.resourceId",
"time": "$.detail.newEvaluationResult.resultRecordedTime"
}
"CloudFormation Stack のドリフトを検出しました。"
"Account ID : <account>"
"Region : <region>"
"ResourceId : <resourceId>"
"Time : <time>"
SNS
EventBridge → SNS のパターンは、 SNS 側のトピックポリシーでevents.amazonaws.com
に対してsns:Publish
を許可する必要があります。
{
"Version": "2008-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "events.amazonaws.com"
},
"Action": "sns:Publish",
"Resource": "arn:aws:sns:ap-northeast-1:111111111111:takakuni-prd-cfn-drift-info-topic"
}
]
}
結果確認
はまりどころは以上になります。結果を確認してみましょう。
Drift Detector のデプロイ
drift_detector.yaml
をデプロイします。
今回は以下の値でスタックのデプロイを行いました。
設定値 | 値 | 備考 |
---|---|---|
SystemName | takakuni | |
Environment | prd | |
MaximumExecutionFrequency | TwentyFour_Hours | |
EmailAddress | メールアドレス |
サブスクリプションの確認
EmailAddress
で入力したアドレス宛に SNS サブスクリプションの確認メールが届いていると思います。
Confirm subscription からサブスクリプションの確認を行います。成功すると以下のような画面に遷移すると思います。
今回はサンプルに以下の CloudFormation スタックをデプロイします。
AWSTemplateFormatVersion: "2010-09-09"
Description: "Sample VPC template"
Mappings:
SubnetConfig:
VPC:
CIDR: "172.16.0.0/16"
Resources:
VPC:
Type: AWS::EC2::VPC
Properties:
EnableDnsSupport: true
EnableDnsHostnames: true
CidrBlock: !FindInMap ["SubnetConfig", "VPC", "CIDR"]
COMPLIANT を確認
AWS Config からルールの再評価を行います。
評価が完了すると Config ルール画面にスタックが表示されます。
NON_COMPLIANT を発生させる
それでは、 VPC の設定値を変更してドリフト状態を引き起こそうと思います。
AWS Config からルールの再評価を行います。
スタックが非準拠になっていることがわかります。
SNS で通知したメールアドレス宛にもメールが届いていました。
通知について
今回、AWS Config ルールの非準拠をトリガーに、 EventBridge ルールを作成しました。
そのため、「NON_COMPLIANT
」からもういっかいチェックして、再度「NON_COMPLIANT
」だった場合はイベントが発生しないためご注意ください。
(後続の SNS 通知もイベントの回数によって変化します。)
定期的にイベントを発生させたい場合は、別のイベントをトリガーにする必要があります。
トリガーイメージ
- COMPLIANT → COMPLIANT(イベント 0回)
- COMPLIANT → NON_COMPLIANT(イベント1回)
- COMPLIANT → NON_COMPLIANT → NON_COMPLIANT(イベント 1回)
- COMPLIANT → NON_COMPLIANT → COMPLIANT → NON_COMPLIANT(イベント 2回)
{
"detail-type": ["Config Rules Compliance Change"],
"source": ["aws.config"],
"detail": {
"messageType": [
"ComplianceChangeNotification"
],
"configRuleName": ["SYSTEMNAME-ENVIRONMENT-config-cfn-drift-rule"],
"newEvaluationResult": {
"complianceType": ["NON_COMPLIANT"]
}
}
}
定期的に配信したい人向け
上記の課題を解決する方法の1つとして、 EventBridge のトリガーを CloudFormation に変更することで、定期的な実行結果をもとにイベントをトリガーできます。
ただ、是正がされていないスタックが多ければ多いほど通知量が多くなるため、ノイズにならないよう注意です。
参考までに以下は、 CloudFormation 経由で EventBridge ルールを作成した場合のコードです。
EventsCfnDriftRule:
Type: AWS::Events::Rule
Properties:
Name: !Sub "${SystemName}-${Environment}-cfn-drift-event-rule"
EventPattern:
source:
- "aws.cloudformation"
detail-type:
- "CloudFormation Drift Detection Status Change"
detail:
status-details:
stack-drift-status:
- "DRIFTED"
detection-status:
- "DETECTION_COMPLETE"
Targets:
- Arn: !Ref SnsCfnDriftTopic
Id: "cfn-drift"
InputTransformer:
InputPathsMap:
"account": "$.account"
"region": "$.region"
"resourceId": "$.detail.stack-id"
"time": "$.time"
InputTemplate: |
"CloudFormation Stack のドリフトを検出しました。"
"Account ID : <account>"
"Region : <region>"
"ResourceId : <resourceId>"
"Time : <time>"
【全文】 drift_detector.yaml
AWSTemplateFormatVersion: "2010-09-09"
Description: "AWS CloudFormation Stack Drift Detector"
Metadata:
AWS::CloudFormation::Interface:
ParameterGroups:
- Label:
default: "General Configuration"
Parameters:
- SystemName
- Environment
- Label:
default: "AWS Config Configuration"
Parameters:
- MaximumExecutionFrequency
- Label:
default: "Amazon SNS Configuration"
Parameters:
- EmailAddress
Parameters:
# General Configuration
SystemName:
Type: String
Description: "System Name"
Environment:
Description: "Environment Name"
Type: String
AllowedValues:
- "prd"
- "stg"
- "dev"
# AWS Config Configuration
MaximumExecutionFrequency:
Type: String
Description: "The maximum frequency with which AWS Config runs evaluations for a rule."
AllowedValues:
- "One_Hour"
- "Three_Hours"
- "Six_Hours"
- "Twelve_Hours"
- "TwentyFour_Hours"
Default: TwentyFour_Hours
# Amazon SNS Configuration
EmailAddress:
Type: String
Description: "Email Address for receiving notification"
Resources:
###################################
# AWS Config Configuration
###################################
ConfigCfnDriftPolicy:
Type: AWS::IAM::ManagedPolicy
Properties:
ManagedPolicyName: !Sub "${SystemName}-${Environment}-config-cfn-drift-policy"
Description: "IAM Policy for AWS CloudFormation Stack Drift Detector"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Action:
- "cloudformation:DetectStackResourceDrift"
- "cloudformation:DescribeStackDriftDetectionStatus"
- "cloudformation:DetectStackDrift"
Resource: "*"
ConfigCfnDriftRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub "${SystemName}-${Environment}-config-cfn-drift-role"
Description: "IAM Role for AWS CloudFormation Stack Drift Detector"
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Principal:
Service:
- "config.amazonaws.com"
Action:
- "sts:AssumeRole"
ManagedPolicyArns:
- "arn:aws:iam::aws:policy/ReadOnlyAccess"
- !Ref ConfigCfnDriftPolicy
ConfigCfnDriftRule:
Type: AWS::Config::ConfigRule
Properties:
ConfigRuleName: !Sub "${SystemName}-${Environment}-config-cfn-drift-rule"
InputParameters:
cloudformationRoleArn: !GetAtt ConfigCfnDriftRole.Arn
Scope:
ComplianceResourceTypes:
- "AWS::CloudFormation::Stack"
Source:
Owner: "AWS"
SourceIdentifier: "CLOUDFORMATION_STACK_DRIFT_DETECTION_CHECK"
MaximumExecutionFrequency: !Ref MaximumExecutionFrequency
###################################
# Amazon SNS Configuration
###################################
SnsCfnDriftTopic:
Type: AWS::SNS::Topic
Properties:
DisplayName: !Sub "${SystemName}-${Environment}-cfn-drift-info-topic"
TopicName: !Sub "${SystemName}-${Environment}-cfn-drift-info-topic"
CfnDriftSubscription:
Type: AWS::SNS::Subscription
Properties:
Endpoint: !Ref EmailAddress
TopicArn: !Ref SnsCfnDriftTopic
Protocol: "email"
SnsCfnDriftTopicPolicy:
Type: AWS::SNS::TopicPolicy
Properties:
Topics:
- !Ref SnsCfnDriftTopic
PolicyDocument:
Statement:
- Effect: "Allow"
Principal:
Service: "events.amazonaws.com"
Action: "sns:Publish"
Resource: !Ref SnsCfnDriftTopic
###################################
# Amazon EventBridge Configuration
###################################
EventsCfnDriftRule:
Type: AWS::Events::Rule
Properties:
Name: !Sub "${SystemName}-${Environment}-cfn-drift-event-rule"
EventPattern:
source:
- "aws.cloudformation"
detail-type:
- "CloudFormation Drift Detection Status Change"
detail:
status-details:
stack-drift-status:
- "DRIFTED"
detection-status:
- "DETECTION_COMPLETE"
Targets:
- Arn: !Ref SnsCfnDriftTopic
Id: "cfn-drift"
InputTransformer:
InputPathsMap:
"account": "$.account"
"region": "$.region"
"resourceId": "$.detail.stack-id"
"time": "$.time"
InputTemplate: |
"CloudFormation Stack のドリフトを検出しました。"
"Account ID : <account>"
"Region : <region>"
"ResourceId : <resourceId>"
"Time : <time>"
まとめ
以上、「定期的にドリフトをチェックしSNSへ通知してみた」でした!
ドリフトの検出が自動でかつマネージドにできるのは、とても便利な機能だと感動しました。この記事がどなたかの参考になれば幸いです。
AWS事業本部コンサルティング部のたかくに(@takakuni_)でした!