こしあん
2025-02-15

AWS Configを使ってCloudFormationのドリフトを検知する


35{icon} {views}


AWS Configを活用し、CloudFormationスタックのドリフトを自動的に「非準拠」として検知・管理する手順を解説した記事です。ルール設定自体よりも、RecorderやIAMロールなど初期セットアップの複雑さが大きなポイントになっています。

はじめに

  • AWS ConfigでCloudFormationのドリフトを検知し、ドリフトがあったものを「非準拠」とするようなルールを適用してみる
  • ドリフトとは、CloudFormation(IaC)の管理しているリソースに対して、IaC以外から変更が入った結果、定義とは異なるリソース設定になったもの。あとからマネジメントコンソールでいじったなどがあり得るケースで、例えばSQSの可視性タイムアウトを60秒で定義しているのに、あとから30秒ひ変更したらドリフトとなる
  • ドリフトしていないものを「準拠」、ドリフトしているものを「非準拠」とするようなConfigルールはマネージドルールのcloudformation-stack-drift-detection-checkで可能。
  • AWS Configの初期設定がちょっと面倒なので、Configルールよりもそこでハマるかもしれない

やり方

  • Configのセットアップが終わっていれば、ルール判定用のIAMロールとConfigルールの定義でOK
  • IAMロールの必要なポリシーは公式ドキュメントによると以下の通りで、
    • cloudformation:DetectStackDrift
    • cloudformation:DetectStackResourceDrift
    • cloudformation:BatchDescribeTypeConfigurations
  • この他にCloudFormationのスタックで定義されているリソースに応じた権限が必要。この例ではSQSをデプロイしているので、以下の権限が必要。スタックの内容に応じた権限が必要というのがなんとも使い勝手が悪いが、こっちのほうが権限に忠実なのでAWSの仕様としては正しい。
    • sqs:GetQueueAttributes
  • ルール定義では、input_parametersにロールのARNを渡す
  • maximum_execution_frequencyルールの実行頻度。以下の値が取り得る:One_Hour | Three_Hours | Six_Hours | Twelve_Hours | TwentyFour_Hours
# ------------------------------------------------------------------------
# CloudFormationスタックドリフト検知に必要なアクションを定義
# ------------------------------------------------------------------------

# CloudFormationドリフト検出で必要となる操作を許可するためのポリシードキュメント
data "aws_iam_policy_document" "aws_config_cfn_role_policy_doc" {
  statement {
    effect = "Allow"
    actions = [
      "cloudformation:DetectStackDrift",
      "cloudformation:DetectStackResourceDrift",
      "cloudformation:BatchDescribeTypeConfigurations",
      # 追加: SQS キューの情報取得用
      "sqs:GetQueueAttributes"
    ]
    resources = ["*"]
  }
}

# ロールに上記ポリシーを付与
resource "aws_iam_role_policy" "aws_config_cfn_role_policy" {
  name   = "cloudformation_stack_drift_permissions"
  role   = aws_iam_role.aws_config_cfn_role.id
  policy = data.aws_iam_policy_document.aws_config_cfn_role_policy_doc.json
}

# ------------------------------------------------------------------------
# Config Ruleの作成
# ------------------------------------------------------------------------

# CloudFormationスタックのドリフト検知を実施するAWS提供のConfigルールを作成
resource "aws_config_config_rule" "cloudformation_stack_drift_detection" {
  name        = "cloudformation-stack-drift-detection-check"
  description = "CloudFormationスタックのドリフト検知を実施するルール"

  source {
    owner             = "AWS"
    source_identifier = "CLOUDFORMATION_STACK_DRIFT_DETECTION_CHECK"
  }

  # IAMロールのARNをパラメータとして渡す
  input_parameters = jsonencode({
    cloudformationRoleArn = aws_iam_role.aws_config_cfn_role.arn
  })

  # 1時間ごとに実行
  maximum_execution_frequency = "One_Hour"

  # ConfigのRecorderやDelivery Channelが有効化されてから設定
  depends_on = [
    aws_config_configuration_recorder_status.main,
    aws_config_delivery_channel.main
  ]
}

結果

事前準備として以下のCloudFormationスタックをデプロイしておく。Terraformでデプロイできる。

AWSTemplateFormatVersion: '2010-09-09'
Description: "CloudFormation を使用して SQS キューを作成するテンプレート"

Resources:
  MySQSQueue:
    Type: AWS::SQS::Queue
    Properties:
      QueueName: MyQueueName  # 任意の名前に変更可能です
      VisibilityTimeout: 60

デプロイ直後

デプロイ直後はConfigルールは以下のように「準拠」になっている

ドリフトを発生させる

SQSのパラメーターを変更してドリフトを発生させる

ドリフト後

無事「非準拠」になっている。これでドリフト検知ができる。

コード

Configを導入するまで

以下のプロセスが必要

  • Config用のロールを作成
  • Configのレコーダーを作成
  • S3バケットを作成し、Configレコーダーが書き込むバケットポリシーを付与
  • Delivery Channelの作成
  • Configレコーダーを開始する

むしろConfigルールそのものよりもこっちのほうが面倒である

デバッグ

ポリシー周りでハマることが多い割にはどこでハマっているのかイマイチわかりづらい。以下のコマンドでデバッグできる

Configレコーダーの状態の確認

aws configservice describe-configuration-recorder-status

以下のようにSUCCESSなど出ていなければポリシーが足りていない。ConfigのロールのIAMポリシーかバケットポリシー

{
    "ConfigurationRecordersStatus": [
        {
            "name": "default",
            "lastStartTime": "2025-02-15T12:55:26.947000+09:00",
            "recording": true,
            "lastStatus": "SUCCESS",
            "lastStatusChangeTime": "2025-02-15T17:55:37.388000+09:00"
        }
    ]
}

ConfigからCloudFormationが認知されているか

今回のConfigルールに限った場合のデバッグ方法。以下のコマンドでCloudFormationが認知されていない場合は、検知用のルールのポリシーが足りていない。CloudFormationでデプロイしたリソース(例:SQSなど)のポリシーを付与すると認識されるようになる。

aws configservice get-discovered-resource-counts --resource-types "AWS::CloudFormation::Stack"
{
    "totalDiscoveredResources": 1,
    "resourceCounts": [
        {
            "resourceType": "AWS::CloudFormation::Stack",
            "count": 1
        }
    ]
}

解説(By o1)

AWS Configを有効にするには、まずAWS Configがリソース情報を収集・評価するための権限を用意し、さらに収集したデータを保存するS3バケットを準備する必要があります。これらの手順を踏むことで、Configルールを正しく機能させ、後から追加するルールによってリソースのコンプライアンス状況を確認できるようになります。

最初に行うべき作業は、AWS Configで使用するIAMロールの作成です。AWS Configは各リソースの状態や変更履歴を収集するために多数のサービスやリソースへアクセスしますが、AWSアカウント内のリソースを横断的に扱うためには適切な権限セットが必要です。そこで、Terraformコードではaws_iam_role.aws_config_roleを定義し、AssumeRoleポリシーとしてconfig.amazonaws.com(AWS Configのサービスプリンシパル)を設定することで、AWS Configがこのロールを引き受けられるようにしています。さらにaws_iam_role_policy_attachment.aws_config_role_attachmentによってマネージドポリシーAWS_ConfigRoleをアタッチし、Configに十分な権限を付与します。

次に、Configuration Recorderを作成します。Configuration RecorderはAWS Configがどのリソースを監視するかを設定する重要なコンポーネントで、ここでは全てのサポートされているAWSリソースを含めるためにall_supportedinclude_global_resource_typesをtrueとしています。Recorderを定義しただけではまだアクティブにはならないため、後ほどaws_config_configuration_recorder_status.mainによってRecorderを有効化する必要があります。

続いて、収集したコンフィグレーションデータを保存するS3バケットとバケットポリシーを準備します。aws_s3_bucket.config_bucketを用いてConfig用のバケットを作成し、バケットポリシーaws_s3_bucket_policy.config_bucket_policyconfig.amazonaws.comに対して書き込み(PutObject)やバケットACL取得(GetBucketAcl)などを許可します。これにより、AWS Configがバケットに対して必要な操作を行えるようになります。

また、Delivery Channelを設定することで、Recorderが取得したデータをどこに配信するかを指定します。aws_config_delivery_channel.mainは配置先バケット名を参照し、Recorderの設定完了後に有効化されるようdepends_onステートメントを使用します。最後に、aws_config_configuration_recorder_status.mainによってRecorder自体を起動し、配信チャネルが正しく動作する状態を整えます。

これらの下準備を行って初めてAWS Configは正常にリソースの変更を記録できるようになります。その上で、ルール(aws_config_config_rule.cloudformation_stack_drift_detectionなど)を作成すれば、AWSリソースの違反やドリフト検出などを評価・通知できるようになるわけです。こうしてConfigの初期構築を正しく行うことで、ルールを設定する際に必要な情報が正常に蓄積され、想定通りのコンプライアンス管理・評価が行えるようになります。

コード

# ------------------------------------------------------------------------
# Configの初期設定
# ------------------------------------------------------------------------

# AWS Configが利用するIAMロールを作成
resource "aws_iam_role" "aws_config_role" {
  name = "aws_config_role"

  # 下のdataソースで定義したポリシードキュメントをAssume Roleポリシーに使用
  assume_role_policy = data.aws_iam_policy_document.aws_config_role_assume_role_policy.json
}

# AWS ConfigがIAMロールを引き受けるためのポリシードキュメントを定義
data "aws_iam_policy_document" "aws_config_role_assume_role_policy" {
  statement {
    actions = ["sts:AssumeRole"]
    principals {
      type        = "Service"
      identifiers = ["config.amazonaws.com"]
    }
  }
}

# AWS Config が各種リソース情報を取得するために必要なアクションが一通り入ったマネージドポリシーのアタッチ
resource "aws_iam_role_policy_attachment" "aws_config_role_attachment" {
  role       = aws_iam_role.aws_config_role.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWS_ConfigRole"
}

# ------------------------------------------------------------------------
# Config Recorderの作成
# ------------------------------------------------------------------------

# AWS Configの録画を行うConfiguration Recorderの作成
resource "aws_config_configuration_recorder" "main" {
  name     = "default"
  role_arn = aws_iam_role.aws_config_role.arn

  recording_group {
    # AWS全体のリソースを録画対象にする
    all_supported                 = true
    include_global_resource_types = true
  }
}

# ------------------------------------------------------------------------
# バケットポリシーの作成
# ------------------------------------------------------------------------

# S3バケットにConfig用ポリシーを適用
resource "aws_s3_bucket_policy" "config_bucket_policy" {
  bucket = aws_s3_bucket.config_bucket.id
  policy = data.aws_iam_policy_document.config_bucket_policy.json
}

# ConfigからS3へのアクセスを許可するバケットポリシー本体
data "aws_iam_policy_document" "config_bucket_policy" {
  # バケットのACLとLocation取得を許可
  statement {
    sid       = "GetBucketAclAndLocation"
    actions   = ["s3:GetBucketAcl", "s3:GetBucketLocation"]
    resources = [aws_s3_bucket.config_bucket.arn]
    principals {
      type        = "Service"
      identifiers = ["config.amazonaws.com"]
    }
  }

  # Configがバケットにオブジェクトを書き込むためのアクセスを許可
  statement {
    sid       = "AllowPutObjectByAWSConfig"
    actions   = ["s3:PutObject"]
    resources = ["${aws_s3_bucket.config_bucket.arn}/*"]
    principals {
      type        = "Service"
      identifiers = ["config.amazonaws.com"]
    }
    # バケット所有者にフルコントロールを付与するACLの指定
    condition {
      test     = "StringEquals"
      variable = "s3:x-amz-acl"
      values   = ["bucket-owner-full-control"]
    }
  }
}

# ------------------------------------------------------------------------
# Delivery Channel の作成
# ------------------------------------------------------------------------

# Configが成果物を置くS3バケットの作成
resource "aws_s3_bucket" "config_bucket" {
  bucket        = var.config_bucket_name
  force_destroy = true
}

# ConfigがS3バケットに情報を配信するためのDelivery Channel設定
resource "aws_config_delivery_channel" "main" {
  name           = "default"
  s3_bucket_name = aws_s3_bucket.config_bucket.bucket

  # Configuration Recorder作成後に実行されるように依存関係を指定
  depends_on = [aws_config_configuration_recorder.main]
}

# ------------------------------------------------------------------------
# Recorderを起動するためのStatusリソース
# ------------------------------------------------------------------------

# Configuration Recorderを実際に起動し、記録を開始する
resource "aws_config_configuration_recorder_status" "main" {
  name       = aws_config_configuration_recorder.main.name
  is_enabled = true

  # 配信チャネルとレコーダーが作成された後に有効化
  depends_on = [
    aws_config_configuration_recorder.main,
    aws_config_delivery_channel.main
  ]
}

# ------------------------------------------------------------------------
# Config用のロール(CloudFormationドリフト検知用)
# ------------------------------------------------------------------------

# CloudFormationのドリフト検知に使うIAMロールを作成
resource "aws_iam_role" "aws_config_cfn_role" {
  name = "aws_config_cfn_role"

  # 下のdataソースで定義したポリシードキュメントをAssume Roleポリシーに使用
  assume_role_policy = data.aws_iam_policy_document.aws_config_cfn_role_assume_role_policy.json
}

# CloudFormationがIAMロールを引き受けるためのポリシードキュメントを定義
data "aws_iam_policy_document" "aws_config_cfn_role_assume_role_policy" {
  statement {
    effect  = "Allow"
    actions = ["sts:AssumeRole"]
    principals {
      type        = "Service"
      identifiers = ["config.amazonaws.com"]
    }
  }
}

# ------------------------------------------------------------------------
# CloudFormationスタックドリフト検知に必要なアクションを定義
# ------------------------------------------------------------------------

# CloudFormationドリフト検出で必要となる操作を許可するためのポリシードキュメント
data "aws_iam_policy_document" "aws_config_cfn_role_policy_doc" {
  statement {
    effect = "Allow"
    actions = [
      "cloudformation:DetectStackDrift",
      "cloudformation:DetectStackResourceDrift",
      "cloudformation:BatchDescribeTypeConfigurations",
      # 追加: SQS キューの情報取得用
      "sqs:GetQueueAttributes"
    ]
    resources = ["*"]
  }
}

# ロールに上記ポリシーを付与
resource "aws_iam_role_policy" "aws_config_cfn_role_policy" {
  name   = "cloudformation_stack_drift_permissions"
  role   = aws_iam_role.aws_config_cfn_role.id
  policy = data.aws_iam_policy_document.aws_config_cfn_role_policy_doc.json
}

# ------------------------------------------------------------------------
# Config Ruleの作成
# ------------------------------------------------------------------------

# CloudFormationスタックのドリフト検知を実施するAWS提供のConfigルールを作成
resource "aws_config_config_rule" "cloudformation_stack_drift_detection" {
  name        = "cloudformation-stack-drift-detection-check"
  description = "CloudFormationスタックのドリフト検知を実施するルール"

  source {
    owner             = "AWS"
    source_identifier = "CLOUDFORMATION_STACK_DRIFT_DETECTION_CHECK"
  }

  # IAMロールのARNをパラメータとして渡す
  input_parameters = jsonencode({
    cloudformationRoleArn = aws_iam_role.aws_config_cfn_role.arn
  })

  # 1時間ごとに実行
  maximum_execution_frequency = "One_Hour"

  # ConfigのRecorderやDelivery Channelが有効化されてから設定
  depends_on = [
    aws_config_configuration_recorder_status.main,
    aws_config_delivery_channel.main
  ]
}

# ------------------------------------------------------------------------
# ドリフトさせるCloudFormationスタック(テストリソース)
# ------------------------------------------------------------------------

# テスト用にSQSのCloudFormationスタックをデプロイ
resource "aws_cloudformation_stack" "sqs_stack" {
  name          = "my-sqs-queue-stack"
  template_body = file("${path.module}/sqs-queue.yaml")

  depends_on = [ 
    aws_config_configuration_recorder_status.main,
    aws_config_delivery_channel.main,
    aws_config_config_rule.cloudformation_stack_drift_detection
  ]
}

所感

  • ルールそのものよりもConfigの初期設定のほうが面倒だった
  • 問題によく出てきたから実装してみたけど、これ実際に使うのかと言われたらCloudFormation前提なのでまぁ…という感じ


Shikoan's ML Blogの中の人が運営しているサークル「じゅ~しぃ~すくりぷと」の本のご案内

技術書コーナー

北海道の駅巡りコーナー


Add a Comment

メールアドレスが公開されることはありません。 が付いている欄は必須項目です