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.
- 🎥 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 / packsviadepends_onso a singleapplylands 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_arnfromtf-mod-aws-kmsto enforce a customer-managed CMK over PII-bearing configuration snapshots delivered to S3. - 🧮 Rules & packs as data.
rulesandconformance_packsaremap(object(...))collections rendered withfor_each— keyed by stable caller strings, with per-itemtagsmerged over module tags. - 🏢 Org-wide rollup, optional. Supply an
aggregatorobject for cross-account / cross-region compliance aggregation (explicit account list or whole AWS Organization). - 🏷️ Tags where they're accepted.
var.tagsflows to the taggable Config resources — rules and the aggregator — and merges with providerdefault_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.
If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:
- ⭐ Star this repository to help others discover this Terraform module.
- 🤝 Connect with me on LinkedIn: linkedin.com/in/microsoftexpert
- ☕ Buy me a coffee: buymeacoffee.com/microsoftexpert
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!
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
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
| 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 |
| 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).
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:PassRoleis explicitly required. The recorder must be handed an IAM role (with the AWS-managedAWS_ConfigRolepolicy, 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.
- Service-linked role. AWS Config uses
AWSServiceRoleForConfig(auto-created viaiam:CreateServiceLinkedRole) unless you supply a custom recorderrole_arn. The role must trustconfig.amazonaws.comand carry the AWS-managedAWS_ConfigRolepolicy (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.coms3:PutObject(with thebucket-owner-full-controlACL condition) ands3:GetBucketAcl. Wires3_bucket_namefromtf-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::ResourceCompliancerecording 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.
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
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"
}
}| 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 |
| 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 keystoneaws_config_configuration_recorderhas no ARN attribute — Config addresses it by name. The module'sarnoutput therefore falls back to the aggregator ARN; for the taggable children, userule_arns/conformance_pack_arns.
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 dashboards15 · 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" }
}| 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.
| 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). |
- ID format. The recorder
idis the recorder name (default"default"). The delivery channelidis 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 packsarn:aws:config:<region>:<account>:conformance-pack/<name>/<id>; the aggregatorarn:aws:config:<region>:<account>:config-aggregator/<id>. The module's primaryarnfalls back to the aggregator ARN, elsenull— userule_arns/conformance_pack_arnsfor the children. - Force-new fields.
name(recorder) anddelivery_channel_nameare FORCE-NEW — changing either destroys and recreates the resource. Conformance packnameis FORCE-NEW.recording_groupandrecording_modeare mutable (you can switch all-resources ↔ inclusion/exclusion list without replacement). tags↔tags_all↔default_tags.var.tagsflows only to the taggable Config resources —aws_config_config_ruleandaws_config_configuration_aggregator. The recorder, delivery channel, and conformance packs accept no tags.tags_all(andrule_tags_all) is the provider-computed merge of resource tags over providerdefault_tags, with resource tags winning on key conflict.default_tagsis 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 / packswithdepends_on. A freshly created role may lag (IAM propagation) — aPutDeliveryChannelorStartConfigurationRecordercall can transiently fail withInsufficientDeliveryPolicyException/ 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::ResourceComplianceis 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
regionvariable; the module records in the inherited provider's Region.include_global_resource_types = truerecords global resources (IAM, etc.); to avoid duplicate global-resource recording across Regions, enable it in one Region only (typically your home Region).
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-bucketandtf-mod-aws-iam-role— keeping blast radius to the Config service plane. for_each, nevercount, forrulesandconformance_packs— keyed by stable caller strings so reorders don't churn the plan.- Primary outputs
id+arn, plus per-collection ARN maps andtags_all.
# Validate without backend or credentials
terraform init -backend=false
terraform validate
terraform fmt -check
plan/applyrequire valid AWS credentials (profile / SSO / OIDC) resolved through the standard provider chain, a configured Region, and the IAM actions listed above (includingiam:PassRoleon the recorder role). The delivery bucket must already carry the Config bucket policy before the delivery channel can be created.
terraform init -backend=false && terraform validate— schema + reference integrity.terraform fmt -check— canonical formatting.terraform planagainst 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_arnin 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.
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
| 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 |
- Service Limits for AWS Config
- Permissions for the Amazon S3 bucket (delivery channel)
- Conformance Packs for AWS Config
- Creating AWS Config Custom Policy Rules (Guard)
- List of AWS Config Managed Rules
- Terraform:
aws_config_configuration_recorder·aws_config_delivery_channel·aws_config_config_rule·aws_config_conformance_pack·aws_config_configuration_aggregator - Sibling modules:
tf-mod-aws-iam-role,tf-mod-aws-s3-bucket,tf-mod-aws-kms,tf-mod-aws-security-hub,tf-mod-aws-guardduty - Module internals:
SCOPE.md
🧡 "Infrastructure as Code should be standardized, consistent, and secure."