AWS Configを使ってリソースの手動変更→Slack通知をCloudFormationで書く

# はじめに
タイトルの通り、1年ほど前にAWS Configを使ってリソースの手動変更が起きた際にSlackで通知するCloudFormationのコードを作成したので、記録に残します。

# 目次
- [実現したこと](#実現したこと)
- [コード](#コード)
  - [ドリフト検出ルールのテンプレート](#ドリフト検出ルールのテンプレート)
  - [Slack通知設定のテンプレート](#slack通知設定のテンプレート)
  - [AWS Configの設定テンプレート](#aws-configの設定テンプレート)
- [参考](#参考)

# 実現したこと
実現したこととしては以下の通りです。
- 前提
  - CloudFormationでAWSのリソースを全て管理していること(=スタックで作成しているリソースが今回の監視対象)

![アーキテクチャ図](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3516746/884d1582-56e9-a6df-99b2-94f87120d349.png)

- 仕組み
  - 1時間に1回ドリフト検出のルール[^1]を走らせる
    - ドリフトが検出された(=リソースの手動変更があった)場合、EventBridgeが発火し、SNSトピック→ChatbotからSlackの任意のチャンネルへ通知がいく

# コード

## ドリフト検出ルールのテンプレート
```yaml:'awsconfig-rule.yml'
AWSTemplateFormatVersion: '2010-09-09'
Resources:
  # AWS config-rule
  CheckForCloudformationStackDriftDetection:
    Type: AWS::Config::ConfigRule
    Properties:
      ConfigRuleName: 'cloudformation-stack-drift-detection-check'
      Description: 'CloudFormationスタックの実際の構成が、予想される構成と異なるかどうか、またはずれているかどうかを確認します。'
      Source:
        Owner: AWS
        SourceIdentifier: 'CLOUDFORMATION_STACK_DRIFT_DETECTION_CHECK'
      MaximumExecutionFrequency: 'One_Hour'
      Scope:
        ComplianceResourceTypes:
          - 'AWS::CloudFormation::Stack'
      InputParameters:
        cloudformationRoleArn: !Sub arn:aws:iam::${AWS::AccountId}:role/config-rules-role
```

## Slack通知設定のテンプレート

```yaml:'awsconfig-notify.yml'
AWSTemplateFormatVersion: '2010-09-09' 
Parameters:
  SlackWorkspaceIdParameter:
    Type: String
  SlackChannelIdParameter:
    Type: String
  SNSTopicConfigNotificationParameter:
    Type: String

Resources:
  # Chatbot
  ChatbotSlackChannelConfigurationConfigNotification:
    Type: AWS::Chatbot::SlackChannelConfiguration
    Properties:
      ConfigurationName: !Sub config-notification
      IamRoleArn: !GetAtt IAMRoleSlackChannelConfigurationRole.Arn
      SlackChannelId: !Ref SlackChannelIdParameter
      SlackWorkspaceId: !Ref SlackWorkspaceIdParameter
      SnsTopicArns:
        - !Ref SNSTopicConfigNotificationParameter

 # Shared Resources
  IAMRoleSlackChannelConfigurationRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub slack-channel-cofiguration-config-role
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Action:
              - sts:AssumeRole
            Effect: Allow
            Principal:
              Service:
                - chatbot.amazonaws.com
      Policies:
        - PolicyName: slack-channel-cofiguration-policy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Action:
                  - cloudwatch:Describe*
                  - cloudwatch:Get*
                  - cloudwatch:List*
                Effect: Allow
                Resource:
                  - '*'
```

## AWS Configの設定テンプレート
```yaml:'awsconfig-template'
AWSTemplateFormatVersion: '2010-09-09'

Resources:
  # IAM Role
  IAMRoleConfigRulesRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: config-rules-role
      Description: IAM role for CloudFormation Drift Detection
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - config.amazonaws.com
                - cloudformation.amazonaws.com
            Action:
              - sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/ReadOnlyAccess
      Policies:
        - PolicyName: AllowAccessToDriftDetection
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - cloudformation:DetectStackResourceDrift
                  - cloudformation:DescribeStackDriftDetectionStatus
                  - cloudformation:DetectStackDrift
                Resource: '*'
                
  # AWS config
  ConfigBucket:
    DeletionPolicy: Retain
    Type: AWS::S3::Bucket
    Properties:
      BucketEncryption:
          ServerSideEncryptionConfiguration:
            - ServerSideEncryptionByDefault:
                SSEAlgorithm: AES256
  ConfigBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref ConfigBucket
      PolicyDocument:
        Version: 2012-10-17
        Statement:
          - Sid: AWSConfigBucketPermissionsCheck
            Effect: Allow
            Principal:
              Service:
                - config.amazonaws.com
            Action: s3:GetBucketAcl
            Resource:
              - !Sub "arn:${AWS::Partition}:s3:::${ConfigBucket}"
            Condition:
              StringEquals:
                AWS:SourceAccount: !Ref "AWS::AccountId"
          - Sid: AWSConfigBucketDelivery
            Effect: Allow
            Principal:
              Service:
                - config.amazonaws.com
            Action: s3:PutObject
            Resource:
              - !Sub "arn:${AWS::Partition}:s3:::${ConfigBucket}/AWSLogs/${AWS::AccountId}/*"
            Condition:
              StringEquals:
                AWS:SourceAccount: !Ref "AWS::AccountId"
          - Sid: AWSConfigBucketSecureTransport
            Action:
              - s3:*
            Effect: Deny
            Resource:
              - !Sub "arn:${AWS::Partition}:s3:::${ConfigBucket}"
              - !Sub "arn:${AWS::Partition}:s3:::${ConfigBucket}/*"
            Principal: "*"
            Condition:
              Bool:
                aws:SecureTransport:
                  false
  ConfigRecorderRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - config.amazonaws.com
            Action:
              - sts:AssumeRole
      Path: /
      ManagedPolicyArns:
        - !Sub "arn:${AWS::Partition}:iam::aws:policy/service-role/AWS_ConfigRole"
  ConfigRecorder:
    Type: AWS::Config::ConfigurationRecorder
    DependsOn:
      - ConfigBucketPolicy
    Properties:
      RoleARN: !GetAtt ConfigRecorderRole.Arn
      RecordingGroup:
        AllSupported: False
        IncludeGlobalResourceTypes: False
        ResourceTypes: 
          - AWS::CloudFormation::Stack
        ExclusionByResourceTypes:
          ResourceTypes: []
        RecordingStrategy:
          UseOnly: INCLUSION_BY_RESOURCE_TYPES
  ConfigDeliveryChannel:
    Type: AWS::Config::DeliveryChannel
    DependsOn:
      - ConfigBucketPolicy
    Properties:
      S3BucketName: !Ref ConfigBucket

  # EventsRule
  EventRuleForConfig:
    Type: AWS::Events::Rule
    Properties:
      Description: 自動リソースに手動で修正が入ったときに検知します
      EventBusName: default
      EventPattern:
        source:
          - aws.config
        detail-type:
          - Config Rules Compliance Change
        detail:
          messageType:
            - ComplianceChangeNotification
          configRuleName:
            - cloudformation-stack-drift-detection-check
          resourceType:
            - AWS::CloudFormation::Stack
          newEvaluationResult:
            complianceType:
              - NON_COMPLIANT
      Name: config-notify-resource-rule
      State: ENABLED
      Targets:
        - Id: snsid
          Arn: !Sub arn:aws:sns:ap-northeast-1:${AWS::AccountId}:config-notification-resource

  # SNS Topic
  SNSTopicConfigNotification:
    Type: AWS::SNS::Topic
    Properties:
      TopicName: config-notification-resource
      DisplayName: config-notification-resource
      Subscription:
        - Endpoint: 'https://global.sns-api.chatbot.amazonaws.com'
          Protocol: https
  SNSTopicPolicyConfigNotification:
    Type: AWS::SNS::TopicPolicy
    Properties:
      PolicyDocument:
        Version: '2008-10-17'
        Id: '__default_policy_ID'
        Statement:
          - Sid: 'AWSEvents_config-notify-resource-rule_snsid'
            Effect: Allow
            Principal:
              Service:
                - 'events.amazonaws.com'
            Action:
              - 'SNS:Publish'
            Resource: 
              - !Ref SNSTopicConfigNotification
      Topics:
        - !Ref SNSTopicConfigNotification
```
# 参考
- [AWSで新しくリソース作成されたときに通知を受け取りたい～Configのリソースタイプで検知しよう～](https://www.sunnycloud.jp/column/20230205-01/)
- [ConfigのConfigurationItemStatus一覧](https://docs.aws.amazon.com/config/latest/APIReference/API_ConfigurationItem.html)
- [Configサポートのリソースタイプ一覧](https://docs.aws.amazon.com/ja_jp/config/latest/developerguide/resource-config-reference.html)

[^1]: スタックのテンプレートと実際のリソースの差分を検出してくれる機能。詳しくは[こちら](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/detect-drift-stack.html)。