LoginSignup
6
4

More than 1 year has passed since last update.

【AWS】CloudFormationのドリフト検出を通知

Last updated at Posted at 2022-01-08

はじめに

CloudFormationで作成したスタック内のリソースを直接設定変更すると、テンプレートで定義された設定内容と実際のリソースの設定内容に差分(以下、「ドリフト」と記述)が生じます。
ドリフトが発生することにより、スタックの更新や削除の際に問題が生じる可能性があります。

今回は「ドリフト検出」の機能を使用して、発生したドリフトの内容の確認やドリフトの解消を実施してみたいと思います。
また、定期的なドリフト検出とSlackへの通知も併せて構成していきます。

構成図

定期的なドリフト検出とSlackへの通知のためにCloudFormation以外に以下のサービスを使用します。
※ドリフト検出を実行するのみであれば不要です。

  • AWS Config
  • Amazon EventBridge
  • Amazon SNS
  • AWS Chatbot

diagram02.png

CloudFormation スタック作成

スタック構成図

以下のような構成でCloudFormationスタックを作成します。
diagram01.png

テンプレート

テンプレートの内容は以下となります。

drift-detection-test.yml
AWSTemplateFormatVersion: 2010-09-09
Description: CloudFormation drift detection test

Parameters:
  VpcName:
    Type: String
    Description: VPC name
    Default: "testVPC"
  VpcCidrBlock:
    Type: String
    Description: VPC CIDR block
    Default: "10.0.0.0/16"
  IgwName:
    Type: String
    Description: Internet gateway name
    Default: "testIGW"
  SubnetName:
    Type: String
    Description: Subnet name
    Default: "testSubnet"
  SubnetCidrBlock:
    Type: String
    Description: Subnet CIDR block
    Default: "10.0.0.0/24"
  PublicRouteTableName:
    Type: String
    Description: Public route table name
    Default: "public-rt"
  SecurityGroupName:
    Type: String
    Description: Security group name
    Default: "testSG"
  SSHAccessIp:
    Type: String
    Description: Source IP address of SSH access
    Default: "0.0.0.0/0"
  InstanceName:
    Type: String
    Description: EC2 instance name
    Default: "testEC2"
  KeyName:
    Type: String
    Description: Key pair name
    Default: "cloudtech-keypair"
  ImageId:
    Type: String
    Description: Image ID of EC2 instance
    Default: "ami-0404778e217f54308"
  BucketName:
    Type: String
    Description: S3 bucket name
    Default: "drift-detection-test01"

Resources:
  testVPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VpcCidrBlock
      EnableDnsSupport: true
      Tags:
        - Key: Name
          Value: !Ref VpcName
  testIGW:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: !Ref IgwName
  AttachGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref testVPC
      InternetGatewayId: !Ref testIGW
  testSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Sub ${AWS::Region}a
      VpcId: !Ref testVPC
      CidrBlock: !Ref SubnetCidrBlock
      Tags:
        - Key: Name
          Value: !Ref SubnetName
  publicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref testVPC
      Tags:
        - Key: Name
          Value: !Ref PublicRouteTableName
  publicRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref publicRouteTable
      DestinationCidrBlock: "0.0.0.0/0"
      GatewayId: !Ref testIGW
  publicRouteTableAssoc:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref testSubnet
      RouteTableId: !Ref publicRouteTable
  testSG:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: !Ref SecurityGroupName
      GroupDescription: !Ref SecurityGroupName
      VpcId: !Ref testVPC
      SecurityGroupIngress:
        -
          IpProtocol: "tcp"
          FromPort: 22
          ToPort: 22
          CidrIp: !Ref SSHAccessIp
      Tags:
        - Key: Name
          Value: !Ref SecurityGroupName
  testEC2:
    Type: AWS::EC2::Instance
    Properties:
      KeyName: !Ref KeyName
      ImageId: !Ref ImageId
      InstanceType: "t2.micro"
      NetworkInterfaces:
        - AssociatePublicIpAddress: true
          DeviceIndex: "0"
          SubnetId: !Ref testSubnet
          GroupSet:
            - !Ref testSG
      Tags:
        - Key: Name
          Value: !Ref InstanceName
  s3Bucket:
    Type: AWS::S3::Bucket
    Properties: 
      BucketName: !Ref BucketName
      AccessControl: Private
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      VersioningConfiguration:
        Status: "Suspended"
      Tags:
        - Key: Name
          Value: !Ref BucketName

ドリフト検出

まずはリソースの設定変更を実施する前に現状ドリフトが発生していないことを確認します。
CloudFormationのスタック一覧画面で対象スタックを選択し、[スタックアクション]-[ドリフトの検出]をクリックします。
スクリーンショット 2021-12-28 12.50.49.png

ドリフトの検出実行後、再度対象スタックを選択し、[スタックアクション]-[ドリフト結果を表示]をクリックします。
スクリーンショット 2021-12-28 12.52.36.png

ドリフトステータスが「IN_SYNC」になっていればスタック内のリソースにドリフトが発生していない状態となります。
スクリーンショット 2021-12-28 12.52.57.png

スタック内のリソース設定変更

作成したスタック内のEC2インスタンス, S3バケットに対して以下の変更を加えます。

EC2インスタンス
インスタンスタイプを「t2.micro」から「t2.small」に変更
S3バケット
バケットを削除

上記設定変更後、再度ドリフト検出を実施してみます。
ドリフトステータスが「DRIFTED」となり、スタック内のリソースにドリフトが発生している状態となります。
また、リソースのドリフトステータスでリソースごとのドリフト発生状況を確認すると、EC2インスタンスは「MODIFIED」、S3バケットは「DELETED」とステータスが変更されていることが確認できます。
スクリーンショット 2021-12-31 17.18.33.png

リソースごとにどのようなドリフトが発生しているのか確認してみます。
EC2インスタンスを選択し、[ドリフトの詳細を表示]をクリックします。
スクリーンショット 2021-12-31 17.24.23.png

違い内にドリフトが発生している項目の一覧、詳細内にスタックを作成・更新した際に使用したテンプレートの内容(予定)と現在の実際の設定(現在)が表示されます。
違い内のドリフト発生項目にチェックを入れると、詳細内でどのようなドリフトが発生しているかが色付けされ確認することができます。
今回の場合はインスタンスタイプが「t2.micro」から「t2.small」に変更されたことが確認できます。
スクリーンショット 2021-12-31 17.27.11.png

ドリフト解消

ドリフトを解消するためには以下の3つの方法があります。

  1. 直接変更したリソースの設定を元に戻す
  2. 直接変更したリソースの設定をテンプレートに反映させ、スタックを更新する
  3. リソースをスタックから取り除き、インポートし直す

方法1:直接変更したリソースの設定を元に戻す

直接変更した設定を元の設定に戻します。
EC2インスタンスは停止後にインスタンスタイプを「t2.micro」に戻し、S3バケットは同じ設定で再作成します。

上記作業実施後にドリフト検出を実行してみると、ドリフトが解消されていることを確認できます。
スクリーンショット 2021-12-31 17.36.48.png

S3バケットについてはCloudFormationで付与されたタグが付いていない状態ではありますが、ドラフトがない状態として認識されています。
スクリーンショット 2021-12-31 17.38.05.png

方法2:直接変更したリソースの設定をテンプレートに反映させ、スタックを更新する

直接変更した設定内容をテンプレートに反映させます。
ただし、S3バケットに関しては削除されているだけで設定内容自体に変更はありません。
今回はEC2インスタンスの設定内容に変更があるため発生しない想定ですが、テンプレートの内容を変更しないままスタックを更新すると以下のエラーが発生します。
スクリーンショット 2021-12-31 17.42.50.png

そのため、EC2インスタンスについてはインスタンスタイプの設定値を修正し、S3バケットの記述についてはいったんコメントアウトをします。
テンプレートの修正ができたら、スタックを更新します。

drift-detection-test.yml
  testEC2:
    Type: AWS::EC2::Instance
    Properties:
      KeyName: !Ref KeyName
      ImageId: !Ref ImageId
      InstanceType: "t2.small"
      NetworkInterfaces:
        - AssociatePublicIpAddress: true
          DeviceIndex: "0"
          SubnetId: !Ref testSubnet
          GroupSet:
            - !Ref testSG
      Tags:
        - Key: Name
          Value: !Ref InstanceName
  # s3Bucket: 
  #   Type: AWS::S3::Bucket
  #   Properties: 
  #     BucketName: !Ref BucketName
  #     AccessControl: Private
  #     PublicAccessBlockConfiguration:
  #       BlockPublicAcls: true
  #       BlockPublicPolicy: true
  #       IgnorePublicAcls: true
  #       RestrictPublicBuckets: true
  #     VersioningConfiguration:
  #       Status: "Suspended"
  #     Tags:
  #       - Key: Name
  #         Value: !Ref BucketName

スタック更新時にEC2インスタンスが再起動されます。 インスタンスタイプはすでに「t2.small」となっていますが、本来インスタンスタイプの変更にはインスタンスの停止が必要となるため再起動が発生するものと思われます。

スタック更新が完了すると、EC2インスタンスについてのドリフトが解消されていることが確認できます。
S3バケットについてはテンプレートでコメントアウトされているので、そもそもスタックに存在しないリソースという扱いとなります。
スクリーンショット 2021-12-31 17.54.30.png

S3バケットを再度スタック内に作成するためには、テンプレートでのコメントアウトを解除し、スタックを更新します。

drift-detection-test.yml
  s3Bucket:
    Type: AWS::S3::Bucket
    Properties: 
      BucketName: !Ref BucketName
      AccessControl: Private
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      VersioningConfiguration:
        Status: "Suspended"
      Tags:
        - Key: Name
          Value: !Ref BucketName

これでEC2インスタンス、S3バケットともにドリフトが解消された状態となります。

EC2インスタンスの設定は新しい設定となり、S3バケットは同名ではありますが別バケットとなります。

方法3:リソースをスタックから取り除き、インポートし直す

スタックを更新する際にリソースの再作成が発生する場合には方法3を用いて、既に作成されているリソースを保持したままドリフトを解消することができます。

方法2でEC2インスタンスのドラフトを解消する際には再起動が発生してしまいましたが、方法3で対応することで再起動を発生させずドラフトを解消することができます。
S3バケットについては方法2を使用して再作成を行っていきます。

テンプレート内のEC2インスタンスの記述にDeletionPolicy属性を追加し、スタックを更新します。
DeletionPolicy属性を「Retain」とすることでスタック削除時にリソースが保持されるようになります。
S3バケットはコメントアウトします。

drift-detection-test.yml
  testEC2:
    Type: AWS::EC2::Instance
    DeletionPolicy: Retain
    Properties:
      KeyName: !Ref KeyName
      ImageId: !Ref ImageId
      InstanceType: "t2.micro"
      NetworkInterfaces:
        - AssociatePublicIpAddress: true
          DeviceIndex: "0"
          SubnetId: !Ref testSubnet
          GroupSet:
            - !Ref testSG
      Tags:
        - Key: Name
          Value: !Ref InstanceName
  # s3Bucket: 
  #   Type: AWS::S3::Bucket
  #   Properties: 
  #     BucketName: !Ref BucketName
  #     AccessControl: Private
  #     PublicAccessBlockConfiguration:
  #       BlockPublicAcls: true
  #       BlockPublicPolicy: true
  #       IgnorePublicAcls: true
  #       RestrictPublicBuckets: true
  #     VersioningConfiguration:
  #       Status: "Suspended"
  #     Tags:
  #       - Key: Name
  #         Value: !Ref BucketName

これでEC2インスタンスについては削除時にリソースが保持される状態となりました。

次にテンプレート内のEC2インスタンスをコメントアウトし、S3バケットのコメントアウトを解除した状態でスタックを更新します。

drift-detection-test.yml
  # testEC2:
  #   Type: AWS::EC2::Instance
  #   DeletionPolicy: Retain
  #   Properties:
  #     KeyName: !Ref KeyName
  #     ImageId: !Ref ImageId
  #     InstanceType: "t2.micro"
  #     NetworkInterfaces:
  #       - AssociatePublicIpAddress: true
  #         DeviceIndex: "0"
  #         SubnetId: !Ref testSubnet
  #         GroupSet:
  #           - !Ref testSG
  #     Tags:
  #       - Key: Name
  #         Value: !Ref InstanceName
  s3Bucket: 
    Type: AWS::S3::Bucket
    Properties: 
      BucketName: !Ref BucketName
      AccessControl: Private
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      VersioningConfiguration:
        Status: "Suspended"
      Tags:
        - Key: Name
          Value: !Ref BucketName

コメントアウトしたEC2インスタンスはスタックには存在しない状態となりますが、更新前に作成されていたEC2インスタンスは保持されます。
S3バケットについては新規作成されます。

スタック詳細画面で[リソース]タブを選択し、「AWS::EC2::Instance」「AWS::S3::Bucket」でそれぞれ検索し、EC2インスタンスはスタックに存在しないこと、S3バケットは新規作成されていることを確認します。
スクリーンショット 2022-01-04 10.04.29.png
スクリーンショット 2022-01-04 10.45.58.png

スタックからEC2インスタンスが除外されたことを確認後、テンプレート内のコメントアウトを解除し、設定値を現在のEC2インスタンスの設定値に変更します。

drift-detection-test.yml
  testEC2:
    Type: AWS::EC2::Instance
    DeletionPolicy: Retain
    Properties:
      KeyName: !Ref KeyName
      ImageId: !Ref ImageId
      InstanceType: "t2.small"
      NetworkInterfaces:
        - AssociatePublicIpAddress: true
          DeviceIndex: "0"
          SubnetId: !Ref testSubnet
          GroupSet:
            - !Ref testSG
      Tags:
        - Key: Name
          Value: !Ref InstanceName

上記テンプレート修正が完了したら、リソースのインポートを実施します。
スタック一覧画面で対象スタックを選択し、[スタックアクション]-[スタックへのリソースのインポート]をクリックします。
スクリーンショット 2022-01-04 10.18.22.png

[次へ]をクリックします。
スクリーンショット 2022-01-04 10.20.15.png

テンプレートを指定し、[次へ]をクリックします。
スクリーンショット 2022-01-04 10.23.15.png

識別子の値にインポートするリソースについての値を入力します。
EC2インスタンスの場合はインスタンスIDを入力します。
入力できたら、[次へ]をクリックします。
スクリーンショット 2022-01-04 10.58.25.png

パラメータを確認し、[次へ]をクリックします。
スクリーンショット 2022-01-04 10.27.07.png

設定の概要を確認し、[リソースをインポート]をクリックします。
スクリーンショット 2022-01-04 10.50.01.png

リソースのインポートが完了したら、ドリフト検出を実行し、ドリフトステータスが「IN_SYNC」になっていることを確認します。

EC2インスタンスの設定は新しい設定となり、S3バケットは同名ではありますが別バケットとなります。

ドリフト検出の定期実行

AWS Configにてドリフトを定期的に検出し、ドリフトが発生してるかどうかを判断するためのルールを作成します。
まずはルール作成時に必要となるIAMロールを先に作成しておきます。

IAMロールの作成

CloudFormationスタックのドリフト検出を実行できる権限を持つIAMロールを作成します。
信頼関係やポリシーについては以下の記事を参考にして作成しました。

Configルールを作成する際にはIAMロールのロールARNを指定することになるので、ロールARNを控えておきます。

Configルールの作成

Config管理画面でルール一覧を開き、[ルールを追加]をクリックします。
スクリーンショット 2021-12-28 16.33.04.png

AWS マネージド型ルールの検索窓に「cloudformation」と入力し検索をかけます。
[cloudformation-stack-drift-detection-check]を選択し、[次へ]をクリックします。
スクリーンショット 2021-12-28 16.33.20.png

今回は頻度を「1時間」に設定します。
スクリーンショット 2021-12-28 16.33.58.png

パラメータ内のキー[cloudformationRoleArn]の値として、事前に作成したIAMロールのARNを入力し、[次へ]をクリックします。
スクリーンショット 2021-12-28 16.46.11.png

確認画面で設定を確認し、[ルールを追加]をクリックします。

ルール一覧画面で作成されたルール名をクリックし、ルール詳細画面を開きます。
スクリーンショット 2021-12-28 16.46.30.png

対象範囲内のリソースで「すべて」を選択し、CloudFormationスタックのドリフトステータス評価状況を確認します。
コンプライアンスが「準拠」となっていれば、ドリフトは発生していない状態となります。
スクリーンショット 2021-12-28 20.16.25.png

通知設定

SNSトピックの作成

通知を構成するためにまずはSNSトピックを作成していきます。

SNS管理画面で[トピック]を選択し、[トピックを作成]をクリックします。
スクリーンショット 2021-12-28 20.32.25.png

設定値を入力し、[トピックの作成]をクリックします。
今回は以下の設定値を使用します。
※以下に記載のない設定値はデフォルトとします。

項目名 設定値
タイプ スタンダード
名前 drift-detection-topic

通知用Slackチャンネルの作成

Slackで通知を受け取るチャンネルをあらかじめ作成しておきます。
チャンネルはパブリック、プライベートのいずれでもOKですが、今回はプライベートチャンネルを作成して通知を構成します。
プライベートの場合、あらかじめAWS Chatbotの招待するためのコマンドをSlackで実行しておく必要があります。

If you configure a private Slack channel, run the /invite @AWS command in Slack to invite the AWS Chatbot to the chat room.

作成したプライベートチャンネル内で/invite @AWSと入力し、実行します。
また、Chatbotチャネル作成時にプライベートチャンネルのIDが必要になりますので控えておきます。

Chatbot用IAMロール・ポリシーの作成

Chatbotチャネルを作成するにあたり、Chatbotチャネルに割り当てるIAMロールとチャネルのメンバーが実行可能なアクションを制御する「Channelガードレール」に使用するポリシーを指定する必要があります。

Chatbotチャネル作成ウィザード内で新たにIAMロール・ポリシーを作成することもできますが、今回はChatbotチャネルに割り当てるIAMロールにアタッチするポリシーをChannelガードレールにも設定したいと思いますのでウィザードのデフォルト設定で作成されるIAMロール・ポリシーの設定値に合わせて事前に作成します。

ポリシーは以下の設定で作成します。

項目名 設定値
名前 AWS-Chatbot-NotificationsOnly-Policy
AWS-Chatbot-NotificationsOnly-Policy
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "cloudwatch:Describe*",
                "cloudwatch:Get*",
                "cloudwatch:List*"
            ],
            "Effect": "Allow",
            "Resource": "*"
        }
    ]
}

また、IAMロールは以下の設定で作成します。

項目名 設定値
名前 AWSChatbot-role
ポリシー AWS-Chatbot-NotificationsOnly-Policy
信頼されたエンティティ chatbot.amazonaws.com

Chatbotチャネルの作成

作成したSlackチャンネルへの通知を構成するためにChatbotチャネルを作成します。

Chatbot管理画面でチャットクライアントで[Slack]を選択し、[クライアントを設定]をクリックします。
スクリーンショット 2022-01-08 13.23.38.png

Slackワークスペースへのサインイン画面にリダイレクトされます。
ワークスペースのURLを入力し、[続行する]をクリックします。
スクリーンショット 2022-01-08 13.25.06.png

資格情報を入力し、[サインイン]をクリックします。
スクリーンショット 2022-01-08 13.25.26.png

AWS ChatbotからSlackワークスペースへのアクセス権限リクエストが表示されるので、[許可する]をクリックします。
スクリーンショット 2022-01-08 13.25.53.png

Management Console画面に戻ったら、[新しいチャネルを設定]をクリックします。
スクリーンショット 2022-01-08 13.26.13.png

設定値を入力し、[設定]をクリックします。
今回は以下の設定値を使用します。
※以下に記載のない設定値はデフォルトとします。

項目名 設定値
設定名 drift-detection-channel
チャネルタイプ プライベート
チャネル ID 通知に使用するチャネルのID
ロール設定 チャネル IAM ロール
チャネル IAM ロール 既存の IAM ロールを使用する
ロール名 AWSChatbot-role
Channel ガードレール AWS-Chatbot-NotificationsOnly-Policy
トピック drift-detection-topic

Slack通知のテストを行います。
作成されたチャネルを選択し、[テストメッセージを送信]をクリックします。
スクリーンショット 2022-01-08 13.27.24.png

Slackの通知用チャンネルでテストメッセージを受信していることを確認します。
スクリーンショット 2022-01-08 13.28.09.png

また、事前に作成していたSNSトピックにサブスクリプション設定が自動的に追加されていることを確認します。
スクリーンショット 2022-01-08 13.28.36.png

EventBridgeルールの作成

ConfigによるCloudFormationスタックのドリフトの定期検出、SNS・ChatbotによるSlackへの通知をそれぞれ構成してきました。
その2つをEventBridgeを使用して紐づけることで、ドリフト検出時のSlack通知を構成していきます。

以下を参考に設定していきます。

EventBridge管理画面で[ルール]を選択し、[ルールを作成]をクリックします。
スクリーンショット 2022-01-08 13.29.10.png

名前に任意のルール名を入力します。
パターンを定義ブロックで[イベントパターン]を選択肢、イベント一致パターンで[カスタムパターン]を選択します。
スクリーンショット 2022-01-08 13.30.34.png

イベントパターンに以下を入力し、[保存]をクリック

イベントパターン
{
    "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"
            ]
        }
    }
}

ターゲットを選択ブロックでターゲットに[SNS トピック]を選択し、トピックで[drift-detection-topic]を選択後、[作成]をクリックします。
スクリーンショット 2022-01-08 13.31.31.png

動作確認

Slack通知の動作確認をするためにドリフトを発生させてみましょう。
EC2インスタンスのインスタンスタイプを変更します。

今回Configルールを設定するにあたって、頻度を「1時間」としましたのでConfigルールが最後に評価を実行した時刻から1時間後に以下の流れでSlackに通知されます。
※すぐに動作確認したい場合は評価を手動で実行することも可能です。

  1. Configルールによる評価が実行される
  2. CloudFormationスタックのコンプライアンスが「非準拠」に変更される
  3. EventBridgeルールによってCloudFormationスタックのコンプライアンスが「非準拠」になったイベントをきっかけにSNSトピックにメッセージが発行される
  4. SNSトピックのサブスクリプションであるChatbotがメッセージを受信
  5. ChatbotがSlackチャンネルへのメッセージ送信を実行

無事1時間後にスタックのコンプライアンスが非準拠であることがSlackに通知されました。
スクリーンショット 2022-01-08 14.11.58.png

あとがき

この記事は CloudTech の課題として作成しました。
動画やハンズオン等で学習を進めることができるので、AWS初学者にはおすすめです。

6
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
4