Skip to content

microsoftexpert/tf-mod-aws-config

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

🟧 AWS Config Terraform Module

Stands up continuous configuration recording and compliance evaluation in one call — the configuration recorder, its delivery channel, recorder status, Config rules, conformance packs, and an optional cross-account/region aggregator, correctly ordered and secure-by-default. Built for the AWS provider v6.x.

Terraform aws module type resources


🧩 Overview

  • 🎥 Recording, fully wired. Creates aws_config_configuration_recorder (the keystone) plus everything that is meaningless without it: the delivery channel, recorder status, Config rules, conformance packs, and an optional aggregator.
  • 🔁 Correct ordering, automatically. AWS Config requires a delivery channel before recording can start, and an active recorder before rules and packs evaluate. The module encodes recorder → delivery channel → status → rules / packs via depends_on so a single apply lands in the right sequence.
  • 🌍 Records everything by default. The secure baseline records all supported resource types including global resources (IAM, etc.) — continuous, complete configuration coverage for the audit trail.
  • 🔐 Encrypted delivery. Wire s3_kms_key_arn from tf-mod-aws-kms to enforce a customer-managed CMK over PII-bearing configuration snapshots delivered to S3.
  • 🧮 Rules & packs as data. rules and conformance_packs are map(object(...)) collections rendered with for_each — keyed by stable caller strings, with per-item tags merged over module tags.
  • 🏢 Org-wide rollup, optional. Supply an aggregator object for cross-account / cross-region compliance aggregation (explicit account list or whole AWS Organization).
  • 🏷️ Tags where they're accepted. var.tags flows to the taggable Config resources — rules and the aggregator — and merges with provider default_tags; the recorder, delivery channel, and conformance packs are not taggable.

💡 Why it matters: AWS Config is the system of record for what your AWS resources looked like, when. For a regulated FI under regulatory oversight, continuous recording plus encrypted delivery and managed compliance rules is the difference between a defensible audit trail and a gap. One consistent module keeps that baseline identical across every account and Region.


❤️ Support this project

If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:

Whether it's a star, a professional connection, or a coffee, every gesture helps keep these modules actively maintained and continually improving. Thank you for being part of the community!


🗺️ Where this fits in the family

tf-mod-aws-config is a governance consumer — it wires together foundation resources (an IAM role, a delivery bucket, a CMK) into the account's compliance recording plane.

flowchart LR
 iam["tf-mod-aws-iam-role<br/>Config service role"]
 s3["tf-mod-aws-s3-bucket<br/>delivery bucket + policy"]
 kms["tf-mod-aws-kms<br/>delivery CMK"]
 sns["SNS topic<br/>(app integration)"]
 cfg["tf-mod-aws-config"]
 hub["tf-mod-aws-security-hub<br/>findings"]
 gd["tf-mod-aws-guardduty"]

 iam -->|"role_arn"| cfg
 s3 -->|"s3_bucket_name"| cfg
 kms -->|"s3_kms_key_arn"| cfg
 sns -->|"sns_topic_arn"| cfg
 cfg -.->|"compliance findings"| hub
 cfg -.->|"config feed"| gd

 style cfg fill:#FF9900,color:#fff,stroke:#cc7a00,stroke-width:2px
Loading

🧬 What this module builds

flowchart TD
 subgraph mod["tf-mod-aws-config"]
 rec["aws_config_configuration_recorder.this<br/>(keystone)<br/>recording_group + recording_mode"]
 dc["aws_config_delivery_channel.this<br/>S3 + KMS + SNS + snapshot freq"]
 st["aws_config_configuration_recorder_status.this<br/>is_enabled = true"]
 rules["aws_config_config_rule.this<br/>for_each rules"]
 packs["aws_config_conformance_pack.this<br/>for_each conformance_packs"]
 agg["aws_config_configuration_aggregator.this<br/>optional, guarded for_each"]
 end

 rec --> st
 rec -. depends_on.-> dc
 dc -. depends_on.-> st
 st -. depends_on.-> rules
 st -. depends_on.-> packs

 style rec fill:#FF9900,color:#fff,stroke:#cc7a00,stroke-width:2px
 style agg stroke-dasharray: 5 5
 style packs stroke-dasharray: 5 5
Loading
Resource Count Created when
aws_config_configuration_recorder.this 1 always (keystone)
aws_config_delivery_channel.this 1 always
aws_config_configuration_recorder_status.this 1 always (is_enabled controls start/stop)
aws_config_config_rule.this 0..N one per rules entry
aws_config_conformance_pack.this 0..N one per conformance_packs entry
aws_config_configuration_aggregator.this 0 or 1 aggregator != null

✅ Provider / Versions

Requirement Version
Terraform >= 1.12.0
hashicorp/aws >= 6.0, < 7.0

The module declares only a required_providers block (providers.tf) and inherits the configured provider. There is no provider {} block and no credential variable — credentials resolve through the standard AWS chain at the root/pipeline level (env vars → SSO/shared credentials → assume_role → instance profile / IRSA → OIDC web identity).


🔑 Required IAM Permissions

Least-privilege actions the Terraform execution identity needs to manage this module.

Action Required for Notes
config:PutConfigurationRecorder, config:DeleteConfigurationRecorder, config:DescribeConfigurationRecorders Recorder lifecycle One recorder per account per Region
config:StartConfigurationRecorder, config:StopConfigurationRecorder, config:DescribeConfigurationRecorderStatus Recorder status Driven by is_enabled
config:PutDeliveryChannel, config:DeleteDeliveryChannel, config:DescribeDeliveryChannels Delivery channel S3 / KMS / SNS / snapshot frequency
config:PutConfigRule, config:DeleteConfigRule, config:DescribeConfigRules Config rules Managed, custom-Lambda, and custom-policy rules
config:PutConformancePack, config:DeleteConformancePack, config:DescribeConformancePacks Conformance packs Only when conformance_packs is set
config:PutConfigurationAggregator, config:DeleteConfigurationAggregator, config:DescribeConfigurationAggregators Aggregator Only when aggregator is set
config:TagResource, config:UntagResource, config:ListTagsForResource Tagging Rules and aggregator only (others untaggable)
iam:PassRole Passing role_arn to the recorder Scope to the Config recorder role ARN; trust must allow config.amazonaws.com
iam:CreateServiceLinkedRole SLR auto-creation for config.amazonaws.com Only when relying on AWSServiceRoleForConfig instead of a custom role

⚠️ iam:PassRole is explicitly required. The recorder must be handed an IAM role (with the AWS-managed AWS_ConfigRole policy, or the service-linked role) so Config can read resource configurations and write to the delivery bucket. Scope this to the recorder role's ARN.

🔒 The delivery-bucket grants (s3:PutObject, s3:GetBucketAcl) are granted to the Config service principal in the bucket policy — not to the Terraform identity. They are configured on the bucket (tf-mod-aws-s3-bucket), not here.


📋 AWS Prerequisites

  • Service-linked role. AWS Config uses AWSServiceRoleForConfig (auto-created via iam:CreateServiceLinkedRole) unless you supply a custom recorder role_arn. The role must trust config.amazonaws.com and carry the AWS-managed AWS_ConfigRole policy (or equivalent least-privilege policy).
  • Delivery S3 bucket (mandatory). Config requires an S3 bucket for the delivery channel, with a bucket policy granting config.amazonaws.com s3:PutObject (with the bucket-owner-full-control ACL condition) and s3:GetBucketAcl. Wire s3_bucket_name from tf-mod-aws-s3-bucket. The bucket should live in the same Region as the recorder. See Permissions for the Amazon S3 bucket.
  • One recorder per Region (hard limit). AWS Config allows exactly one configuration recorder per account per Region. Instantiating this module twice in the same account+Region fails on the second recorder — design accordingly.
  • Recorder before rules. Config rules and conformance packs require recording to be active; the module orders recorder → delivery channel → status → rules / packs.
  • Aggregator (optional). Organization aggregation requires AWS Organizations with all-features enabled and a delegated administrator (or the management account), plus an IAM role Config can assume to read Organization details.
  • Conformance packs. A conformance pack deploys an AWS-managed service-linked role and may stage artifacts in a delivery bucket; deleting a pack requires care (disable AWS::Config::ResourceCompliance recording first to avoid CI cost spikes).
  • Quotas (per Service Limits for AWS Config):
  • 1,000 Config rules per Region per account — not adjustable. Rules inside conformance packs count toward this limit.
  • 50 conformance packs per account; 130 rules per conformance pack — not adjustable.
  • 50 tags per resource — not adjustable.
  • 50 configuration aggregators per account (adjustable); up to 10,000 accounts per aggregator.

📁 Module Structure

tf-mod-aws-config/
├── providers.tf # required_providers (aws >= 6.0, < 7.0); no provider block
├── variables.tf # name → role/recording → delivery → status → rules → packs → aggregator → tags
├── main.tf # recorder (this) → delivery channel → status → rules / packs / aggregator
├── outputs.tf # id + arn + recorder/delivery/rule/pack/aggregator attrs + tags_all
├── README.md # this file
└── SCOPE.md # in/out-of-scope, IAM permissions, prerequisites, gotchas

⚙️ Quick Start

Smallest secure call — continuous recording of everything, KMS-encrypted delivery, recording started:

module "config" {
  source = "git::https://github.com/microsoftexpert/tf-mod-aws-config?ref=v1.0.0"

  role_arn       = module.config_role.arn  # from tf-mod-aws-iam-role (trusts config.amazonaws.com)
  s3_bucket_name = module.config_bucket.id # from tf-mod-aws-s3-bucket (with Config bucket policy)
  s3_kms_key_arn = module.config_kms.arn   # from tf-mod-aws-kms — CMK over PII config data

  # Secure defaults already on: record all + global resources, recording started,
  # 24h snapshot delivery. No further input needed for the baseline.

  tags = {
    Environment = "prod"
    CostCenter  = "1234"
  }
}

🔌 Cross-Module Contract

Consumes

Input Type Source module
role_arn string (IAM role ARN) tf-mod-aws-iam-role (or the service-linked role)
s3_bucket_name string (bucket id) tf-mod-aws-s3-bucket (with the Config bucket policy)
s3_kms_key_arn string (KMS key ARN) tf-mod-aws-kms
sns_topic_arn string (SNS topic ARN) app-integration / SNS module
rules[*].source.source_identifier (custom Lambda) string (Lambda ARN) custom-rule Lambda module
aggregator.organization_aggregation_source.role_arn string (IAM role ARN) tf-mod-aws-iam-role

Emits

Output Description Consumed by
id Recorder id (Config uses the recorder name as its id) references / audit
arn Primary ARN — falls back to the aggregator ARN (the recorder exposes no ARN); else null governance
recorder_name Configuration recorder name CLI / audit
recorder_id Recorder id (same as the name) references
role_arn IAM role the recorder assumes audit
recording_enabled Whether recording is started monitoring
delivery_channel_id / delivery_channel_name Delivery channel id / name audit
rule_ids Map of rule key → Config rule id dashboards
rule_arns Map of rule key → Config rule ARN dashboards / audit
rule_tags_all Map of rule key → all tags incl. default_tags governance
conformance_pack_arns Map of pack key → conformance pack ARN audit
aggregator_arn / aggregator_id Aggregator ARN / id (when configured; else null) org compliance views
tags_all All tags incl. default_tags on the aggregator (when configured; else null) governance/audit

ℹ️ Why no single arn? The keystone aws_config_configuration_recorder has no ARN attribute — Config addresses it by name. The module's arn output therefore falls back to the aggregator ARN; for the taggable children, use rule_arns / conformance_pack_arns.


📚 Example Library

1 · Minimal secure baseline (record everything, started)
module "config" {
  source = "git::https://github.com/microsoftexpert/tf-mod-aws-config?ref=v1.0.0"

  role_arn       = module.config_role.arn
  s3_bucket_name = module.config_bucket.id
  # recording_group defaults: all_supported + include_global_resource_types = true
  # is_enabled defaults to true → recording starts
}
2 · Customer-managed KMS over delivery (PII baseline)
module "config_kms" {
  source = "git::https://github.com/microsoftexpert/tf-mod-aws-kms?ref=v1.0.0"
  alias  = "casey/config-delivery"
}

module "config" {
  source = "git::https://github.com/microsoftexpert/tf-mod-aws-config?ref=v1.0.0"

  role_arn       = module.config_role.arn
  s3_bucket_name = module.config_bucket.id
  s3_kms_key_arn = module.config_kms.arn # CMK must be in the same Region as the bucket
}
3 · Tags (merge with provider default_tags)
# Caller's provider block owns default_tags; the module never sets it.
provider "aws" {
  default_tags { tags = { Owner = "security", ManagedBy = "terraform" } }
}

module "config" {
  source = "git::https://github.com/microsoftexpert/tf-mod-aws-config?ref=v1.0.0"

  role_arn       = module.config_role.arn
  s3_bucket_name = module.config_bucket.id

  tags = {
    Environment = "prod" # resource tag — wins over default_tags on key conflict
    DataClass   = "internal"
  }
}
# Tags apply to the taggable children (rules, aggregator); recorder/channel/packs are untaggable.
# module.config.rule_tags_all["<rule>"] == { Owner, ManagedBy, Environment, DataClass }
4 · Managed Config rules (CIS-aligned)
module "config" {
  source = "git::https://github.com/microsoftexpert/tf-mod-aws-config?ref=v1.0.0"

  role_arn       = module.config_role.arn
  s3_bucket_name = module.config_bucket.id

  rules = {
    encrypted-volumes = {
      source = { owner = "AWS", source_identifier = "ENCRYPTED_VOLUMES" }
    }
    s3-public-read-prohibited = {
      source = { owner = "AWS", source_identifier = "S3_BUCKET_PUBLIC_READ_PROHIBITED" }
    }
    rds-storage-encrypted = {
      source = { owner = "AWS", source_identifier = "RDS_STORAGE_ENCRYPTED" }
    }
    iam-password-policy = {
      maximum_execution_frequency = "TwentyFour_Hours"
      source                      = { owner = "AWS", source_identifier = "IAM_PASSWORD_POLICY" }
    }
  }
}
5 · Rule with input parameters and a resource-type scope
rules = {
  required-tags = {
    description      = "Resources must carry CostCenter and Environment tags"
    input_parameters = jsonencode({ tag1Key = "CostCenter", tag2Key = "Environment" })
    scope = {
      compliance_resource_types = ["AWS::EC2::Instance", "AWS::S3::Bucket"]
    }
    source = { owner = "AWS", source_identifier = "REQUIRED_TAGS" }
    tags   = { Severity = "medium" }
  }
}
6 · Custom Lambda rule (event-triggered)
rules = {
  custom-naming-check = {
    source = {
      owner             = "CUSTOM_LAMBDA"
      source_identifier = module.naming_check_lambda.arn # Lambda ARN
      source_detail = [{
        event_source = "aws.config"
        message_type = "ConfigurationItemChangeNotification"
      }]
    }
  }
}
7 · Custom Policy (Guard) rule, proactive + detective
rules = {
  guard-s3-encryption = {
    evaluation_modes = ["DETECTIVE", "PROACTIVE"]
    source = {
      owner = "CUSTOM_POLICY"
      custom_policy_details = {
        policy_runtime = "guard-2.x.x"
        policy_text    = file("${path.module}/guard/s3_encryption.guard")
      }
      source_detail = [{
        event_source = "aws.config"
        message_type = "ConfigurationItemChangeNotification"
      }]
    }
  }
}
8 · Conformance pack from an inline template
module "config" {
  source = "git::https://github.com/microsoftexpert/tf-mod-aws-config?ref=v1.0.0"

  role_arn       = module.config_role.arn
  s3_bucket_name = module.config_bucket.id

  conformance_packs = {
    casey-baseline = {
      template_body      = file("${path.module}/packs/casey-baseline.yaml")
      delivery_s3_bucket = module.config_bucket.id
      input_parameters   = { S3BucketName = module.config_bucket.id }
    }
  }
}
# Conformance packs are NOT taggable; templates have no drift detection.
9 · Conformance pack from an S3 template URI
conformance_packs = {
  operational-best-practices = {
    template_s3_uri = "s3://my-config-templates/operational-best-practices-for-cis.yaml"
  }
}
# template_s3_uri must be in the same Region; set exactly one of template_body / template_s3_uri.
10 · Narrowed recording (inclusion list, no global resources)
module "config" {
  source = "git::https://github.com/microsoftexpert/tf-mod-aws-config?ref=v1.0.0"

  role_arn       = module.config_role.arn
  s3_bucket_name = module.config_bucket.id

  # Opt-out of the record-everything default (documented exception)
  recording_group = {
    all_supported                 = false
    include_global_resource_types = false
    resource_types                = ["AWS::EC2::Instance", "AWS::S3::Bucket", "AWS::RDS::DBInstance"]
    recording_strategy            = { use_only = "INCLUSION_BY_RESOURCE_TYPES" }
  }
}
11 · Exclusion-based recording strategy
recording_group = {
  all_supported                 = false
  include_global_resource_types = false
  exclusion_by_resource_types   = { resource_types = ["AWS::EC2::NetworkInterface"] }
  recording_strategy            = { use_only = "EXCLUSION_BY_RESOURCE_TYPES" }
}
12 · Daily recording cadence with a per-type override
recording_mode = {
  recording_frequency = "DAILY"
  recording_mode_override = {
    description         = "Track security groups continuously"
    resource_types      = ["AWS::EC2::SecurityGroup"]
    recording_frequency = "CONTINUOUS"
  }
}
13 · SNS delivery notifications + tuned snapshot frequency
module "config" {
  source = "git::https://github.com/microsoftexpert/tf-mod-aws-config?ref=v1.0.0"

  role_arn           = module.config_role.arn
  s3_bucket_name     = module.config_bucket.id
  sns_topic_arn      = module.config_topic.arn # delivery notifications
  delivery_frequency = "Six_Hours"             # default is TwentyFour_Hours
  s3_key_prefix      = "config"
}
14 · Cross-account aggregator (explicit account list)
module "config" {
  source = "git::https://github.com/microsoftexpert/tf-mod-aws-config?ref=v1.0.0"

  role_arn       = module.config_role.arn
  s3_bucket_name = module.config_bucket.id

  aggregator = {
    name = "casey-security-rollup"
    account_aggregation_source = {
      account_ids = ["111122223333", "444455556666"]
      all_regions = true
    }
    tags = { Team = "security" }
  }
}
# module.config.aggregator_arn /.arn → org compliance dashboards
15 · Organization-wide aggregator
aggregator = {
  name = "casey-org-rollup"
  organization_aggregation_source = {
    role_arn    = module.config_org_role.arn # role Config assumes to read the Organization
    all_regions = true
  }
}
# Requires AWS Organizations + delegated admin / management account.
16 · End-to-end composition — IAM role + delivery bucket + CMK + Config baseline
# Customer-managed CMK for the delivery bucket and Config delivery
module "config_kms" {
  source = "git::https://github.com/microsoftexpert/tf-mod-aws-kms?ref=v1.0.0"
  alias  = "casey/config"
}

# Secure delivery bucket (SSE-KMS, public access blocked) with the Config bucket policy
module "config_bucket" {
  source      = "git::https://github.com/microsoftexpert/tf-mod-aws-s3-bucket?ref=v1.0.0"
  bucket      = "casey-aws-config-delivery"
  kms_key_arn = module.config_kms.arn
  #... attach the config.amazonaws.com bucket policy (s3:PutObject + s3:GetBucketAcl)
}

# IAM role Config assumes (trusts config.amazonaws.com, AWS_ConfigRole managed policy)
module "config_role" {
  source = "git::https://github.com/microsoftexpert/tf-mod-aws-iam-role?ref=v1.0.0"
  name   = "casey-config-recorder"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect    = "Allow"
      Principal = { Service = "config.amazonaws.com" }
      Action    = "sts:AssumeRole"
    }]
  })
  managed_policy_arns = ["arn:aws:iam::aws:policy/service-role/AWS_ConfigRole"]
}

# This module — recorder + channel + status + rules
module "config" {
  source = "git::https://github.com/microsoftexpert/tf-mod-aws-config?ref=v1.0.0"

  role_arn       = module.config_role.arn
  s3_bucket_name = module.config_bucket.id
  s3_kms_key_arn = module.config_kms.arn

  rules = {
    encrypted-volumes         = { source = { owner = "AWS", source_identifier = "ENCRYPTED_VOLUMES" } }
    s3-public-read-prohibited = { source = { owner = "AWS", source_identifier = "S3_BUCKET_PUBLIC_READ_PROHIBITED" } }
    rds-storage-encrypted     = { source = { owner = "AWS", source_identifier = "RDS_STORAGE_ENCRYPTED" } }
  }

  tags = { Environment = "prod", DataClass = "internal" }
}

📥 Inputs

Name Type Default Description
name string "default" Recorder name. FORCE-NEW. One recorder per account per Region.
role_arn string (ARN) required IAM role Config assumes; trusts config.amazonaws.com. Requires iam:PassRole.
recording_group object({...}) {} (all + global) What the recorder captures; secure default records all supported + global types.
recording_mode object({...}) null Recording cadence (CONTINUOUS/DAILY) with optional per-type override.
delivery_channel_name string "default" Delivery channel name. FORCE-NEW.
s3_bucket_name string required Delivery bucket id (with the Config bucket policy).
s3_key_prefix string null Optional key prefix in the delivery bucket.
s3_kms_key_arn string (ARN) null CMK encrypting delivered objects (same Region as the bucket).
sns_topic_arn string (ARN) null SNS topic for delivery notifications.
delivery_frequency string "TwentyFour_Hours" Snapshot delivery cadence; null omits the block.
is_enabled bool true Whether recording is started (secure baseline).
rules map(object({...})) {} Config rules keyed by stable name; per-rule tags.
conformance_packs map(object({...})) {} Conformance packs keyed by stable name (not taggable).
aggregator object({...}) null Optional cross-account / cross-region aggregator.
tags map(string) {} Tags for taggable resources (rules, aggregator); merge with default_tags.

See variables.tf for full heredoc schemas and validation rules.


🧾 Outputs

Name Description
id Recorder id (Config uses the name as its id).
arn Primary ARN — aggregator ARN fallback (recorder has no ARN); else null.
recorder_name / recorder_id Recorder name / id.
role_arn IAM role the recorder assumes.
recording_enabled Whether recording is started.
delivery_channel_id / delivery_channel_name Delivery channel id / name.
rule_ids / rule_arns / rule_tags_all Per-rule id / ARN / merged tags maps.
conformance_pack_arns Per-pack ARN map.
aggregator_arn / aggregator_id Aggregator ARN / id (else null).
tags_all All tags incl. default_tags on the aggregator (else null).

🧠 Architecture Notes

  • ID format. The recorder id is the recorder name (default "default"). The delivery channel id is likewise the channel name. There is exactly one recorder per account per Region.
  • ARN formats. The configuration recorder and delivery channel expose no ARN — Config addresses them by name. Config rules carry arn:aws:config:<region>:<account>:config-rule/<config-rule-id>; conformance packs arn:aws:config:<region>:<account>:conformance-pack/<name>/<id>; the aggregator arn:aws:config:<region>:<account>:config-aggregator/<id>. The module's primary arn falls back to the aggregator ARN, else null — use rule_arns / conformance_pack_arns for the children.
  • Force-new fields. name (recorder) and delivery_channel_name are FORCE-NEW — changing either destroys and recreates the resource. Conformance pack name is FORCE-NEW. recording_group and recording_mode are mutable (you can switch all-resources ↔ inclusion/exclusion list without replacement).
  • tagstags_alldefault_tags. var.tags flows only to the taggable Config resources — aws_config_config_rule and aws_config_configuration_aggregator. The recorder, delivery channel, and conformance packs accept no tags. tags_all (and rule_tags_all) is the provider-computed merge of resource tags over provider default_tags, with resource tags winning on key conflict. default_tags is configured in the caller's provider block — never here.
  • Ordering & eventual consistency. AWS Config requires the delivery channel to exist before the recorder can be started, and an active recorder before rules/packs evaluate. The module encodes recorder → delivery channel → status → rules / packs with depends_on. A freshly created role may lag (IAM propagation) — a PutDeliveryChannel or StartConfigurationRecorder call can transiently fail with InsufficientDeliveryPolicyException / authorization errors and succeed on retry.
  • Destroy ordering. Reverses creation: rules and packs are removed, recording is stopped, then the delivery channel and recorder are deleted. Stopping the recorder before deleting the delivery channel is required — Terraform handles this from the dependency graph. Deleting a conformance pack while AWS::Config::ResourceCompliance is recorded can spike CI volume; disable that type first if needed.
  • us-east-1 globals. N/A — AWS Config is a regional service. There is no region variable; the module records in the inherited provider's Region. include_global_resource_types = true records global resources (IAM, etc.); to avoid duplicate global-resource recording across Regions, enable it in one Region only (typically your home Region).

🧱 Design Principles

Secure-by-default posture and every opt-out, explicitly:

Posture Default Opt-out
Recording scope record all supported resource types incl. global resources narrow via recording_group (inclusion/exclusion list)
Recorder status started (is_enabled = true) set is_enabled = false (discouraged — rules can't evaluate)
Delivery encryption SSE-KMS via caller-supplied CMK (s3_kms_key_arn) leave null → rely on the bucket's default encryption
Snapshot delivery TwentyFour_Hours tune delivery_frequency, or null to omit the block
Recording cadence continuous (AWS default) set recording_mode to DAILY with per-type overrides
Baseline rules none baked in — caller selects CIS-aligned managed rules populate rules (examples 4–7)
Aggregation none supply aggregator (account list or Organization)

The module bakes in recording posture (record everything, start recording, encrypted delivery) but deliberately leaves which compliance rules to the caller — rule selection is a policy decision owned by the security/compliance team. Examples 4–7 provide an CIS-aligned starting set.

Other principles:

  • One composite, one keystone. The recorder owns only the resources meaningless without it (channel, status, rules, packs, aggregator). The delivery bucket and IAM role are deliberately out of scope (referenced by name/ARN) — shared, long-lived governance resources owned by tf-mod-aws-s3-bucket and tf-mod-aws-iam-role — keeping blast radius to the Config service plane.
  • for_each, never count, for rules and conformance_packs — keyed by stable caller strings so reorders don't churn the plan.
  • Primary outputs id + arn, plus per-collection ARN maps and tags_all.

🚀 Runbook

# Validate without backend or credentials
terraform init -backend=false
terraform validate
terraform fmt -check

plan / apply require valid AWS credentials (profile / SSO / OIDC) resolved through the standard provider chain, a configured Region, and the IAM actions listed above (including iam:PassRole on the recorder role). The delivery bucket must already carry the Config bucket policy before the delivery channel can be created.


🧪 Testing

  • terraform init -backend=false && terraform validate — schema + reference integrity.
  • terraform fmt -check — canonical formatting.
  • terraform plan against a sandbox account to confirm the recorder, delivery channel, status, and any rules/packs materialize in the correct order.
  • Assert module.config.recorder_name, recording_enabled, rule_arns, and (if configured) aggregator_arn in your root-module test harness.
  • After apply, confirm in the console / CLI (aws configservice describe-configuration-recorder-status) that recording is STARTED and the delivery channel is healthy.

💬 Example Output

module.config.aws_config_configuration_recorder.this: Creation complete after 2s [id=default]
module.config.aws_config_delivery_channel.this: Creation complete after 1s [id=default]
module.config.aws_config_configuration_recorder_status.this: Creation complete after 3s [id=default]
module.config.aws_config_config_rule.this["encrypted-volumes"]: Creation complete [id=encrypted-volumes]
module.config.aws_config_config_rule.this["s3-public-read-prohibited"]: Creation complete

Outputs:
id = "default"
recorder_name = "default"
recording_enabled = true
delivery_channel_id = "default"
rule_arns = {
 "encrypted-volumes" = "arn:aws:config:us-east-1:123456789012:config-rule/config-rule-ab12cd"
 "s3-public-read-prohibited" = "arn:aws:config:us-east-1:123456789012:config-rule/config-rule-ef34gh"
}
arn = null # no aggregator configured; recorder exposes no ARN

🔍 Troubleshooting

Symptom Likely cause Fix
MaxNumberOfConfigurationRecordersExceededException A recorder already exists in this account+Region (hard limit of 1) Import the existing recorder, or manage Config from a single module instance per Region
InsufficientDeliveryPolicyException on the delivery channel Bucket policy missing the config.amazonaws.com grants, or KMS key policy denies Config Attach the Config bucket policy (s3:PutObject + s3:GetBucketAcl); grant Config kms:GenerateDataKey/kms:Decrypt on the CMK
AccessDenied / iam:PassRole on apply Terraform identity lacks iam:PassRole on the recorder role Grant iam:PassRole scoped to role_arn; ensure the role trusts config.amazonaws.com
Recorder created but not recording is_enabled = false, or status applied before the delivery channel Set is_enabled = true; the module's depends_on already orders channel before status
Rules show INSUFFICIENT_DATA / never evaluate Recording not active, or rule scope excludes the resources Confirm recording_enabled = true; widen recording_group / rule scope
Conformance pack stuck CREATE_FAILED Template error, or delivery_s3_bucket unwritable by Config Validate the template; ensure the pack delivery bucket grants Config write access
Global resources recorded twice / extra cost include_global_resource_types = true in multiple Regions Enable global-resource recording in one Region only
Tag drift on every plan A tag also set by provider default_tags with a different value Let resource tags win, or remove the overlap from default_tags
arn output is null Expected — the recorder has no ARN and no aggregator is configured Use rule_arns / conformance_pack_arns, or add an aggregator
Conformance-pack delete spikes CI volume AWS::Config::ResourceCompliance still recording on delete Disable that resource type's recording before destroying the pack

🔗 Related Docs


🧡 "Infrastructure as Code should be standardized, consistent, and secure."

Packages

 
 
 

Contributors

Languages