0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AWS Configのルールと修復アクションをTerraformで自動構築する

Posted at

はじめに

AWS Configは、アカウント運用上のコンプライアンス要件をルールに落とし込み、違反しているリソースを検知してアクションを行うところまでを自動化することができる。

ルールについても、多岐にわたるAWSのマネージドなルールと、それに対する自動修復アクションが容易されているため、アカウント開始後すぐにでも利用開始できるようになっている。

チームがある程度の大きさの規模になり、アカウントを複数人で運用することになった際、ガバナンスのためのトイルを減らしてくれる便利なサービスなので、是非活用できるようにしておきたい。
※AWSに不慣れなメンバがJOINした際の事故防止にも役に立つ。

本記事に必要な前提知識は以下の通り。

IAMの準備

AWS Configはサービスリンクロールがあるので、基本はこれを使えば良い。

サービスリンクロールを使わない場合は、以下のようにAWS Configに対して権限を付与しておこう。
AWS Configのチェックは多岐に渡るため、一つ一つ設定していくとそれだけで大きな手間になってしまうため、AWS_ConfigRoleのマネージドポリシーを使用する。

AWS ConfigはAmazon S3に情報を記録するため、カスタムポリシーで権限を付与しておく。
オブジェクトの出力先が決まっているので、最小権限構成にするためにresourcesで形式を合わせておく。

resource "aws_iam_role" "config" {
  name               = local.iam_config_role_name
  assume_role_policy = data.aws_iam_policy_document.config_assume.json
}

data "aws_iam_policy_document" "config_assume" {
  statement {
    effect = "Allow"

    principals {
      type        = "Service"
      identifiers = ["config.amazonaws.com"]
    }

    actions = ["sts:AssumeRole"]
  }
}

data "aws_iam_policy" "config_managed_policy" {
  name = "AWS_ConfigRole"
}

resource "aws_iam_role_policy_attachment" "config_managed_policy" {
  role       = aws_iam_role.config.id
  policy_arn = data.aws_iam_policy.config_managed_policy.arn
}

resource "aws_iam_role_policy" "config" {
  name   = local.iam_config_policy_name
  role   = aws_iam_role.config.id
  policy = data.aws_iam_policy_document.config_custom.json
}

data "aws_iam_policy_document" "config_custom" {
  statement {
    effect = "Allow"

    actions = [
      "s3:PutObject",
      "s3:PutObjectAcl"
    ]

    resources = [
      "${aws_s3_bucket.example.arn}/AWSLogs/${data.aws_caller_identity.current.account_id}/*"
    ]

    condition {
      test     = "StringLike"
      variable = "s3:x-amz-acl"

      values = [
        "bucket-owner-full-control",
      ]
    }
  }
  statement {
    effect = "Allow"

    actions = [
      "s3:GetBucketAcl",
    ]

    resources = [
      "${aws_s3_bucket.example.arn}",
    ]
  }
}

レコーダーの設定

まず最初に、AWS Configの情報を記録するためにレコーダーを設定する。

レコーダーの情報を格納するAmazon S3バケットの設定

特に難しいことはないので、以下のようにAmazon S3バケットを作っておく。
バージョニングはあってもなくても良いが、この後のAWS Configのルールでバージョニング有無をチェックするようにするので、そこでコンプライアンス違反とならないよう設定をしておく。

resource "aws_s3_bucket" "example" {
  bucket = local.s3_bucket_name

  force_destroy = true
}

resource "aws_s3_bucket_ownership_controls" "example" {
  bucket = aws_s3_bucket.example.id

  rule {
    object_ownership = "BucketOwnerEnforced"
  }
}

resource "aws_s3_bucket_public_access_block" "example" {
  bucket = aws_s3_bucket.example.id

  block_public_acls       = false
  block_public_policy     = false
  ignore_public_acls      = false
  restrict_public_buckets = false
}

resource "aws_s3_bucket_versioning" "example" {
  bucket = aws_s3_bucket.example.id

  versioning_configuration {
    status = "Enabled"
  }
}

レコーダーを設定する

レコーダーはTerraformのaws_config_configuration_recorderのリソースを扱う。
role_arnには、最初に作成したIAMロールを設定する。

また、aws_config_delivery_channelのリソースで、上記で作成したAmazon S3に接続を行い、aws_config_configuration_recorder_statusis_enabled = trueして有効化することでAWS Configの利用が開始できる。

レコーダーはリージョンに1つあれば良い(というか、2つ以上作ろうとしてもエラーになる)ので、既に存在する場合は定義しなくても良い。

aws_config_delivery_channelaws_config_configuration_recorder_statusには、インプットとなるパラメータに良い感じの依存関係がないため、depends_onで明示して新規作成時のエラーを防止する。

resource "aws_config_configuration_recorder" "example" {
  name = local.config_recorder_name

  role_arn = aws_iam_role.config.arn
}

resource "aws_config_delivery_channel" "example" {
  depends_on = [aws_config_configuration_recorder.example]

  name = local.config_delivery_channel_name

  s3_bucket_name = aws_s3_bucket.example.bucket
}

resource "aws_config_configuration_recorder_status" "example" {
  depends_on = [aws_config_delivery_channel.example]

  name = aws_config_configuration_recorder.example.name

  is_enabled = true
}

ルールの設定

ルールの設定には、Terraformのaws_config_config_ruleのリソースを用いる。
今回は、設定が簡単なAmazon S3のバージョニング設定を行っていない場合に、コンプライアンス違反として検知するマネージドなルールを適用する。scopeおよびinput_parametersisMfaDeleteEnabledは省略可能だが、練習のためにちゃんと書いておく。

resource "aws_config_config_rule" "example1" {
  depends_on = [aws_config_configuration_recorder.example]

  name = local.config_rule_name1

  scope {
    compliance_resource_types = [
      "AWS::S3::Bucket",
    ]
  }

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

  input_parameters = jsonencode(
    {
      isMfaDeleteEnabled = "false",
    }
  )
}

これでterraform applyすると、マネコンからもルールが参照できるようになる。

キャプチャ1.png

上記のリンクを踏んでいくと、詳細の画面で設定を確認できる。

image.png

御覧の通り、非準拠のリソースは存在しない。
せっかくなので、ここで、あえて非準拠のAmazon S3のリソースを作成してみる。
Terraformのaws_s3_bucket_versioningのリソースのversioning_configurationstatus = "Disabled"にすることで、今回のコンプライアンス要件を違反することになる。

resource "aws_s3_bucket" "example_test" {
  bucket = local.s3_bucket_for_test_name
}

resource "aws_s3_bucket_ownership_controls" "example_test" {
  bucket = aws_s3_bucket.example_test.id

  rule {
    object_ownership = "BucketOwnerEnforced"
  }
}

resource "aws_s3_bucket_public_access_block" "example_test" {
  bucket = aws_s3_bucket.example_test.id

  block_public_acls       = false
  block_public_policy     = false
  ignore_public_acls      = false
  restrict_public_buckets = false
}

resource "aws_s3_bucket_versioning" "example_test" {
  bucket = aws_s3_bucket.example_test.id

  versioning_configuration {
    status = "Disabled"
  }
}

これをterraform applyしてしばらく待つと、ルールの画面下部、「対象範囲内のリソース」が以下のようになる。なお、分かりやすくするために、リソースは「すべて」を表示している。config-example-bucketはレコーダー記録用のバケットで、最初にバージョニングを有効にしているため、コンプライアンス準拠となっているが、新規に作ったバケットはコンプライアンス非準拠の結果になっている。

image.png

これで、ルールに違反したリソースを自動で検知できるようになった。

修復アクションの設定

さて、せっかくルール違反を検知できるようになったのだから、違反したものを検知したら可能な限り速やかに正しい状態にしておきたい。AWS Configには修復アクションという機能があるので、設定をしていこう。

修正アクションはTerraformのaws_config_remediation_configurationのリソースを使用する。
config_rule_nameでルールとの紐づけを行う。
修正アクションそのものは、AWS Systems ManagerのAutomation機能を呼び出すことになるが、aws_config_remediation_configurationが呼び出しを行ってくれるため、自分でのAmazon EventBridgeの設定は不要だ。

Amazon S3のバージョニング設定は、予めAWSが用意したAWS Systems Manager Automationのドキュメントがあるため、それを指定すれば良い。

maximum_automatic_attemptsretry_attempt_secondsのパラメータは、エラーになった際のリトライ回数と間隔、execution_controlsはAWS Systems Managerの同時実行数の制御のために用いるパラメータであるため、必要に応じて修正する。

resource "aws_config_remediation_configuration" "example1" {
  config_rule_name = aws_config_config_rule.example1.name

  resource_type  = "AWS::S3::Bucket"
  target_type    = "SSM_DOCUMENT"
  target_id      = "AWS-ConfigureS3BucketVersioning"
  target_version = "1"

  parameter {
    name         = "AutomationAssumeRole"
    static_value = aws_iam_role.ssm_remediation.arn
  }
  parameter {
    name           = "BucketName"
    resource_value = "RESOURCE_ID"
  }
  parameter {
    name         = "VersioningState"
    static_value = "Enabled"
  }

  automatic                  = true
  maximum_automatic_attempts = 5
  retry_attempt_seconds      = 60

  execution_controls {
    ssm_controls {
      concurrent_execution_rate_percentage = 25
      error_percentage                     = 35
    }
  }
}

ここで注意しなければいけないのが、AWS Systems Managerに修正のための権限が必要であるということだ。
以下のように、IAMロールを作成して、今回必要になるAmazon S3のバージョニング設定の変更の権限を不要しよう。
なお、修正アクションはアカウント内のすべてのリソースに適用する可能性があるため、resourcesに関しては最小権限で絞ることはできず*を設定している。

resource "aws_iam_role" "ssm_remediation" {
  name               = local.iam_ssm_remediation_role_name
  assume_role_policy = data.aws_iam_policy_document.ssm_remediation_assume.json
}

data "aws_iam_policy_document" "ssm_remediation_assume" {
  statement {
    effect = "Allow"

    principals {
      type        = "Service"
      identifiers = ["ssm.amazonaws.com"]
    }

    actions = ["sts:AssumeRole"]
  }
}

resource "aws_iam_role_policy" "ssm_remediation" {
  name   = local.iam_ssm_remediation_policy_name
  role   = aws_iam_role.ssm_remediation.id
  policy = data.aws_iam_policy_document.ssm_remediation_custom.json
}

data "aws_iam_policy_document" "ssm_remediation_custom" {
  statement {
    effect = "Allow"

    actions = [
      "s3:GetBucketVersioning",
      "s3:PutBucketVersioning",
    ]

    resources = [
      "*",
    ]
  }
}

上記をApplyしてしばらく待つと、修復アクションが自動実行され、以下の通り、コンプライアンスが準拠した状態になる。

image.png

Amazon S3側でも、バージョニング設定が変わっていることを確認できる。

image.png

バージョニングの設定は一度Enabledにすると、Terraformでは一度aws_s3_bucket_versioningのリソースを削除しないと、Disabledに戻すことはできない。更新時に誤って戻してしまうことはないので、期待する状態を維持しやすくなったと言える。

カスタム修復アクションの設定

マネージドルールに対して、AWSがプレフィックスしている修復アクションが無いものもある。
例えば、S3_LIFECYCLE_POLICY_CHECK(その名の通りAmazon S3のライフサイクルポリシーの設定有無や、設定内容を細かくチェックする定義ができるルール)だ。

本項では、これを自動で修復するアクションを作っていく。

AWS Systems Manager Automationドキュメントの作成

カスタム修復アクションもAWS Systems Manager Automationのドキュメントで制御する。
以下のようにカスタム修復アクションを作成しよう。

内容の詳細は本記事の趣旨からは外れるため割愛する。
Amazon S3のライフサイクルポリシーを取得し、

  • 重複するルール名がないこと
  • 1000件を超えないこと

を確認したうえで、既存のルールに修復用のルールを追加してライフサイクルポリシーを更新している。

今回は練習用であまり深く気にせずこの設定を行っているが、ライフサイクルポリシーは複数設定ができるため、実際は競合時の動作が問題ないかをよく検証したうえで設定を導入した方が良い。
競合時の考え方はAWS公式のユーザーガイドを参照。

resource "aws_ssm_document" "example" {
  name            = local.ssm_document_name
  document_type   = "Automation"
  document_format = "YAML"

  content = file("${path.module}/ssmdoument.yaml")
}
ssmcodument.yaml
schemaVersion: "0.3"
description: |
    Remediation Action for AWS Config managed rule S3_LIFECYCLE_POLICY_CHECK.
assumeRole: "{{ AutomationAssumeRole }}"
parameters:
    AutomationAssumeRole:
        type: AWS::IAM::Role::Arn
        default: ""
    BucketName:
        type: AWS::S3::Bucket::Name
    DaysUntilTransit:
        type: Integer
        allowedPattern: ^[1-9][0-9]{0,1023}$
    RuleId:
        type: String
        allowedPattern: ^[a-zA-Z0-9_-]{1,1024}$
outputs:
    - VerifyTransitionRule.VerifyTransitionRuleResponse
    - VerifyTransitionRule.LifecycleConfigurationRule

mainSteps:
    - name: PutTransitionRule
      action: aws:executeScript
      timeoutSeconds: 120
      inputs:
        Runtime: python3.8
        Handler: handler
        InputPayload:
            BucketName: "{{ BucketName }}"
            DaysUntilTransit: "{{ DaysUntilTransit }}"
            RuleId: "{{ RuleId }}"            
        Script: |
            import time
            import boto3
            import botocore

            def get_bucket_rules(s3_client, bucket_name):
                try:
                    response = s3_client.get_bucket_lifecycle_configuration(Bucket=bucket_name)
                    return response["Rules"]
                except botocore.exceptions.ClientError as error:
                    response = error.response
                    error_code = response["Error"]["Code"]
                    error_message = response["Error"]["Message"]
                    if error_code == "NoSuchLifecycleConfiguration" and "does not exist" in error_message:
                        return []
                    raise Exception from error

            def handler(event, context):
                bucket_name = event["BucketName"]
                days_until_transit = event["DaysUntilTransit"]
                rule_id = event["RuleId"]

                s3_client = boto3.client("s3")
                bucket_rules = get_bucket_rules(s3_client=s3_client, bucket_name=bucket_name)

                if bucket_rules and len(bucket_rules) == 1000:
                    raise Exception(f"S3 BUCKET {bucket_name} CANNOT HAVE MORE THAN 1,000 LIFECYCLE CONFIGURATION RULES")

                if bucket_rules:
                    bucket_rule_ids = []
                    for rule in bucket_rules:
                        bucket_rule_ids.append(rule["ID"])
                    if rule_id in bucket_rule_ids:
                        raise Exception(
                            f"S3 BUCKET {bucket_name} ALREADY CONTAINS A LIFECYCLE CONFIGURATION WITH ID {rule_id}"
                        )

                transitions = {
                    "ID": rule_id,
                    "Filter": {"Prefix": ""},
                    "Status": "Enabled",
                    "Transitions": [{"Days": days_until_transit, "StorageClass": "INTELLIGENT_TIERING"}],
                }

                bucket_rules.append(transitions)

                print(bucket_rules)

                try:
                    s3_client.put_bucket_lifecycle_configuration(
                        Bucket=bucket_name, LifecycleConfiguration={"Rules": bucket_rules}
                    )
                    time.sleep(5)
                except botocore.exceptions.ClientError as error:
                    raise Exception from error

    - name: VerifyTransitionRule
      action: aws:executeScript
      timeoutSeconds: 120
      maxAttempts: 5
      inputs:
        Runtime: python3.8
        Handler: handler
        InputPayload:
            BucketName: "{{ BucketName }}"
            RuleId: "{{ RuleId }}"
        Script: |
            import boto3
            import botocore

            def handler(event, context):
                bucket_name = event["BucketName"]
                rule_id = event["RuleId"]

                s3_client = boto3.client("s3")

                try:
                    response = s3_client.get_bucket_lifecycle_configuration(Bucket=bucket_name)
                    rules = response["Rules"]
                    for rule in rules:
                        if rule_id == rule.get("ID"):
                            success_message = (
                                "Verification of transit configured for Amazon S3 Bucket is successful."
                            )
                            return {"VerifyTransitionRule": success_message, "Rule": rule}
                    raise Exception(
                        f"FAILED TO VERIFY LIFECYCLE CONFIGURATION RULE WITH ID {rule_id} ON S3 BUCKET {bucket_name}"
                    )
                except botocore.exceptions.ClientError as error:
                    raise Exception from error
      outputs:
        - Name: VerifyTransitionRuleResponse
          Type: String
          Selector: $.Payload.VerifyTransitionRule
        - Name: LifecycleConfigurationRule
          Type: StringMap
          Selector: $.Payload.Rule

AWS Systems ManagerのIAM権限の付与

上記の通り、スクリプト内でGet/PutLifecycleConfigurationのAPIを呼び出しているため。IAM権限の更新が必要だ。もともと作っていたポリシーに以下の権限を追加しよう。

data "aws_iam_policy_document" "ssm_remediation_custom" {
  statement {
    effect = "Allow"

    actions = [
      "s3:GetBucketVersioning",
+     "s3:GetLifecycleConfiguration",
      "s3:PutBucketVersioning",
+     "s3:PutLifecycleConfiguration",
    ]

    resources = [
      "*",
    ]
  }
}

AWS Configルールと修復アクションの設定

上記で作成したAWS Systems Manager Automationドキュメントを以下のように呼び出す。
今回は、バケットの移行設定が40日になっていないものを、40日に補正する修復アクションを実行する。

基本的にプレフィックスの修復アクションの呼び出しと変わらないが、特筆すべき点は、target_idtarget_versionを、上記で作成したリソースへの参照にしているところだ。特に、target_versionは固定にしていると、AWS Systems Manager Automationに更新されず思わぬ動作をする可能性があるので注意が必要だ。

resource "aws_config_config_rule" "example2" {
  depends_on = [aws_config_configuration_recorder.example]

  name = local.config_rule_name2

  scope {
    compliance_resource_types = [
      "AWS::S3::Bucket",
    ]
  }

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

  input_parameters = jsonencode(
    {
      targetTransitionDays = "40",
    }
  )
}

resource "aws_config_remediation_configuration" "example2" {
  config_rule_name = aws_config_config_rule.example2.name

  resource_type  = "AWS::S3::Bucket"
  target_type    = "SSM_DOCUMENT"
  target_id      = aws_ssm_document.example.name
  target_version = aws_ssm_document.example.latest_version

  parameter {
    name         = "AutomationAssumeRole"
    static_value = aws_iam_role.ssm_remediation.arn
  }
  parameter {
    name           = "BucketName"
    resource_value = "RESOURCE_ID"
  }
  parameter {
    name         = "DaysUntilTransit"
    static_value = "40"
  }
  parameter {
    name         = "RuleId"
    static_value = "AWSConfigRemedation_S3Lifecycle_Transition"
  }

  automatic                  = true
  maximum_automatic_attempts = 5
  retry_attempt_seconds      = 60

  execution_controls {
    ssm_controls {
      concurrent_execution_rate_percentage = 25
      error_percentage                     = 35
    }
  }
}

上記をApplyした後に、

resource "aws_s3_bucket_lifecycle_configuration" "example_test" {
  bucket = aws_s3_bucket.example_test.id

  rule {
    id = "rule"

    status = "Enabled"

    transition {
      days          = 30
      storage_class = "STANDARD_IA"
    }
  }
}

と、ライフサイクルポリシーの移行までの期間が30日の設定を行うと、追加されたルールの詳細画面で、

キャプチャ5.png

といったかたちで検知したい違反が見つかり、しばらく待つとカスタム修復アクションによる修復で以下のようにステータスが変わる。

image.png

Amazon S3のコンソールでも、修復アクション呼び出しのパラメータで指定したAWSConfigRemedation_S3Lifecycle_Transitionでライフサイクルルールが作成されているのが確認できる。

キャプチャ6.png

これで、修復ルールを自分でも作成できるようになった!

参考:エラー発生時のトラブルシュート方法まとめ

記事を書くにあたり参考にしていた情報。

また、カスタム修復アクション内のスクリプトのエラーについては、AWS Systems Manager Automationのコンソールで確認ができる。デバッグ時の参考になれば。

キャプチャ7.png

キャプチャ8.png

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?