CloudWatch log groups should have retention period of at least 365 days
CloudWatch log data is often the only record of application errors, authentication events, and API activity available to incident investigators. When retention falls below 365 days, you lose the ability to correlate events across seasonal patterns or investigate breaches discovered months after initial compromise.
CloudWatch Logs storage costs vary by region, log class, and volume, but for most workloads it is a minor line item compared to losing forensic evidence. Setting retention explicitly also prevents runaway spend: log groups default to indefinite retention and can quietly accumulate terabytes if left unmanaged.
Retrofit consideration
Lambda, API Gateway, ECS, and other services auto-create log groups on first invocation, and none of these show up in your Terraform state. Before running a bulk remediation, you need a full inventory: run DescribeLogGroups and filter for groups missing a retentionInDays value. Importing them into state works, but only if the resource block already includes retention_in_days; importing without it means the next apply strips the policy. A Config auto-remediation or a scheduled Lambda is often the faster path for groups you don't own in Terraform.
Implementation
Choose the approach that matches how you manage Terraform.
Use the compliance.tf module to enforce this control by default. See get started with compliance.tf.
module "lambda" {
source = "soc2.compliance.tf/terraform-aws-modules/lambda/aws"
version = ">=8.0.0"
create_package = false
function_name = "abc123"
handler = "index.lambda_handler"
local_existing_package = "lambda_function.zip"
runtime = "python3.12"
}
module "lambda" {
source = "pcidss.compliance.tf/terraform-aws-modules/lambda/aws"
version = ">=8.0.0"
create_package = false
function_name = "abc123"
handler = "index.lambda_handler"
local_existing_package = "lambda_function.zip"
runtime = "python3.12"
}
module "lambda" {
source = "hipaa.compliance.tf/terraform-aws-modules/lambda/aws"
version = ">=8.0.0"
create_package = false
function_name = "abc123"
handler = "index.lambda_handler"
local_existing_package = "lambda_function.zip"
runtime = "python3.12"
}
module "lambda" {
source = "nist80053.compliance.tf/terraform-aws-modules/lambda/aws"
version = ">=8.0.0"
create_package = false
function_name = "abc123"
handler = "index.lambda_handler"
local_existing_package = "lambda_function.zip"
runtime = "python3.12"
}
module "lambda" {
source = "nistcsf.compliance.tf/terraform-aws-modules/lambda/aws"
version = ">=8.0.0"
create_package = false
function_name = "abc123"
handler = "index.lambda_handler"
local_existing_package = "lambda_function.zip"
runtime = "python3.12"
}
module "lambda" {
source = "fedrampmoderate.compliance.tf/terraform-aws-modules/lambda/aws"
version = ">=8.0.0"
create_package = false
function_name = "abc123"
handler = "index.lambda_handler"
local_existing_package = "lambda_function.zip"
runtime = "python3.12"
}
module "lambda" {
source = "cisv80ig1.compliance.tf/terraform-aws-modules/lambda/aws"
version = ">=8.0.0"
create_package = false
function_name = "abc123"
handler = "index.lambda_handler"
local_existing_package = "lambda_function.zip"
runtime = "python3.12"
}
module "lambda" {
source = "nist800171.compliance.tf/terraform-aws-modules/lambda/aws"
version = ">=8.0.0"
create_package = false
function_name = "abc123"
handler = "index.lambda_handler"
local_existing_package = "lambda_function.zip"
runtime = "python3.12"
}
module "lambda" {
source = "nydfs23.compliance.tf/terraform-aws-modules/lambda/aws"
version = ">=8.0.0"
create_package = false
function_name = "abc123"
handler = "index.lambda_handler"
local_existing_package = "lambda_function.zip"
runtime = "python3.12"
}
module "lambda" {
source = "ffiec.compliance.tf/terraform-aws-modules/lambda/aws"
version = ">=8.0.0"
create_package = false
function_name = "abc123"
handler = "index.lambda_handler"
local_existing_package = "lambda_function.zip"
runtime = "python3.12"
}
module "lambda" {
source = "cfrpart11.compliance.tf/terraform-aws-modules/lambda/aws"
version = ">=8.0.0"
create_package = false
function_name = "abc123"
handler = "index.lambda_handler"
local_existing_package = "lambda_function.zip"
runtime = "python3.12"
}
module "lambda" {
source = "rbicybersecurity.compliance.tf/terraform-aws-modules/lambda/aws"
version = ">=8.0.0"
create_package = false
function_name = "abc123"
handler = "index.lambda_handler"
local_existing_package = "lambda_function.zip"
runtime = "python3.12"
}
module "lambda" {
source = "rbiitfnbfc.compliance.tf/terraform-aws-modules/lambda/aws"
version = ">=8.0.0"
create_package = false
function_name = "abc123"
handler = "index.lambda_handler"
local_existing_package = "lambda_function.zip"
runtime = "python3.12"
}
module "lambda" {
source = "fedramplow.compliance.tf/terraform-aws-modules/lambda/aws"
version = ">=8.0.0"
create_package = false
function_name = "abc123"
handler = "index.lambda_handler"
local_existing_package = "lambda_function.zip"
runtime = "python3.12"
}
module "lambda" {
source = "hipaasecurity2003.compliance.tf/terraform-aws-modules/lambda/aws"
version = ">=8.0.0"
create_package = false
function_name = "abc123"
handler = "index.lambda_handler"
local_existing_package = "lambda_function.zip"
runtime = "python3.12"
}
module "lambda" {
source = "nistcsfv11.compliance.tf/terraform-aws-modules/lambda/aws"
version = ">=8.0.0"
create_package = false
function_name = "abc123"
handler = "index.lambda_handler"
local_existing_package = "lambda_function.zip"
runtime = "python3.12"
}
module "lambda" {
source = "nist80053rev4.compliance.tf/terraform-aws-modules/lambda/aws"
version = ">=8.0.0"
create_package = false
function_name = "abc123"
handler = "index.lambda_handler"
local_existing_package = "lambda_function.zip"
runtime = "python3.12"
}
module "lambda" {
source = "pcidssv321.compliance.tf/terraform-aws-modules/lambda/aws"
version = ">=8.0.0"
create_package = false
function_name = "abc123"
handler = "index.lambda_handler"
local_existing_package = "lambda_function.zip"
runtime = "python3.12"
}
If you use terraform-aws-modules/lambda/aws, set the right module inputs for this control. You can later migrate to the compliance.tf module with minimal changes because it is compatible by design.
module "lambda" {
source = "terraform-aws-modules/lambda/aws"
version = ">=8.0.0"
create_package = false
function_name = "abc123"
handler = "index.lambda_handler"
local_existing_package = "lambda_function.zip"
runtime = "python3.12"
}
Use AWS provider resources directly. See docs for the resources involved: aws_cloudwatch_log_group.
resource "aws_cloudwatch_log_group" "this" {
kms_key_id = "arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012"
name = "/pofix/abc123"
retention_in_days = 365
}
What this control checks
Every aws_cloudwatch_log_group resource must have retention_in_days set to 365 or higher. AWS only accepts a fixed set of values: 365, 400, 545, 731, 1096, 1827, 2192, 2557, 2922, 3288, and 3653. Omitting retention_in_days also fails; the group has no retention policy and logs accumulate indefinitely. Values below 365 (such as 30, 90, or 180) fail regardless of intent. There is no account-level default in AWS, so each log group needs its own setting.
Common pitfalls
Indefinite retention (unset) flagged as non-compliant
It seems counterintuitive: a group with no expiry technically retains logs longer than 365 days. But Config's
cw-loggroup-retention-period-checkand most compliance scanners flag it as non-compliant because there is no explicit bounded retention period. Always set a numeric value of365or higher, even if your intent is to keep logs indefinitely.Log groups created outside Terraform
Lambda, API Gateway, ECS, and RDS auto-create log groups on first invocation (e.g.,
/aws/lambda/<function-name>), and all of them default to never-expire. The cleanest fix is to pre-create these groups in Terraform withretention_in_daysset before the service writes to them. If pre-creation isn't practical, a Config auto-remediation SSM document or a scheduled Lambda that callsPutRetentionPolicyacross all groups is the next best option.Only specific integer values are valid
retention_in_daysaccepts a fixed enumeration: 1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1096, 1827, 2192, 2557, 2922, 3288, and 3653. Pass an arbitrary integer like366or730and the API returns an error. The minimum value that passes this control is365.Terraform import drift for externally managed groups
Import a log group with
terraform import aws_cloudwatch_log_group.example /my/log/groupwithoutretention_in_daysin the resource block and the nextterraform applyremoves the existing retention policy, leaving the group at indefinite retention. Always add the argument to the resource block before importing.
Audit evidence
Auditors typically start with the Config rule cw-loggroup-retention-period-check, with minRetentionTime set to 365, and a compliance report or exported CSV showing all evaluated log groups as COMPLIANT. For deeper assurance, they may pull a DescribeLogGroups API response directly, checking that every group's retentionInDays value is 365 or above.
Supporting evidence can include CloudWatch console screenshots showing per-group retention settings and CloudTrail entries for PutRetentionPolicy calls, which establish when retention was set and by whom.
Framework-specific interpretation
SOC 2: CC7.2 and CC7.3 expect organizations to monitor system components and keep enough log data to detect and respond to anomalies. A 365-day window covers a full audit period plus meaningful lookback, which is what Type II examiners ask to see.
PCI DSS v4.0: Requirement 10.7 is explicit: 12 months total retention, with the most recent three months immediately available for analysis. Setting retention_in_days to 365 meets the 12-month requirement. If you want the active window in CloudWatch and archive older data to S3, you need a separate lifecycle policy to cover the three-months-available requirement.
HIPAA Omnibus Rule 2013: 45 CFR 164.312(b) requires audit controls that record and examine activity on systems processing ePHI. The Omnibus Rule's six-year document retention requirement sets a longer bar, but 365 days of CloudWatch logs satisfies the active monitoring and breach investigation obligations, which is what most auditors focus on in a technical review.
NIST SP 800-53 Rev 5: Maps to AU-11. The control asks for an organization-defined retention period consistent with records retention policy; 365 days covers the minimum most agencies define for operational log data.
NIST Cybersecurity Framework v2.0: A full year of log history supports both DE.CM continuous monitoring and RS.AN incident analysis. Without sufficient retention, analysts lack context for intrusions that dwell for months before detection.
FedRAMP Moderate Baseline Rev 4: AU-11 at the Moderate baseline calls for retention long enough to support after-the-fact investigation of security incidents. The standard FedRAMP expectation is one year online and three years total; 365 days covers the online portion.
Related controls
Tool mappings
Use these identifiers to cross-reference this control across tools, reports, and evidence.
Compliance.tf Control:
cloudwatch_log_group_retention_period_365AWS Config Managed Rule:
CW_LOGGROUP_RETENTION_PERIOD_CHECKCheckov Checks:
CKV_AWS_338,CKV_AWS_66Powerpipe Control:
aws_compliance.control.cloudwatch_log_group_retention_period_365Prowler Check:
cloudwatch_log_group_retention_policy_specific_days_enabledAWS Security Hub Control:
CloudWatch.16KICS Query:
ef0b316a-211e-42f1-888e-64efe172b755
Last reviewed: 2026-03-09