定期的にドリフトをチェックしSNSへ通知してみた

CloudFormation スタックのドリフト検出を定期実行して、ドリフトが検出された際に SNS に通知する仕組みを作ってみました。
2023.02.19

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは!AWS事業本部コンサルティング部のたかくに(@takakuni_)です。

今回は、定期的に CloudFormation スタックのドリフトを確認し、ドリフトが検出された時に SNS で通知する仕組みを作ってみようと思います。

スタックのドリフトとは

CloudFormation スタックのドリフトとは、スタックでデプロイしたリソースに対して、手動(時には自動)で設定値が変更されていないかを確認する機能です。

注意点として CloudFormation のドリフト検出は「コードとして記載されている設定値」と「現在の設定値」の差分を確認する機能のため、コードとして記載されていない部分(暗黙的なデフォルト値)については、検出されない仕様となっています。

そのため、ドリフト検出させたい部分は明示的に記載する必要があります。詳しくはこちらもご覧ください。

今回の構成

今回の構成図は以下の通りです。

AWS Config のマネージドルールで提供されている 「CLOUDFORMATION_STACK_DRIFT_DETECTION_CHECK」 で定期的にスタックのドリフトを評価します。

Config ルールが 「NON_COMPLIANT」 に変わったイベントをトリガーに EventBridge Rule を発火させます。

Amazon SNS を利用して登録したサブスクリプション宛に結果を通知します。

作成したコード

今回作成したコードは以下の通りです。

※ 少し長いので折りたたんでいます。クリックして適宜お使いください。

drift_detector.yaml

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 スタックをデプロイします。

sample.yaml

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_)でした!