diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6709dd5..57d8efc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,6 +12,11 @@ repos: args: ['--allow-missing-credentials'] # Avoid false positives - id: check-yaml # Added YAML validation - id: check-merge-conflict # Added merge conflict detection + - id: check-json # Added JSON validation + - id: check-toml # Added TOML validation + - id: detect-private-key # Added private key detection + - id: mixed-line-ending + args: ['--fix=lf'] # Ensure consistent line endings - repo: https://github.com/antonbabenko/pre-commit-terraform rev: v1.83.5 # Updated to latest stable version hooks: @@ -25,7 +30,21 @@ repos: - id: terraform_tflint # Added terraform linter args: - --args=--config=.tflint.hcl -# - id: terraform_checkov # Added security scanner -# args: -# - --args=--quiet -# - --args=--skip-check CKV_AWS_1 # Skip specific rules if needed + - id: terraform_checkov # Added security scanner + args: + - --args=--quiet + - --args=--framework terraform + - --args=--skip-check CKV_AWS_18 # Skip EBS encryption check for flexibility + - --args=--skip-check CKV_AWS_144 # Skip backup encryption check for flexibility + - repo: https://github.com/Yelp/detect-secrets + rev: v1.4.0 + hooks: + - id: detect-secrets + args: ['--baseline', '.secrets.baseline'] + exclude: '.*/tests/.*' + - repo: https://github.com/crate-ci/typos + rev: v1.16.23 + hooks: + - id: typos + types: [markdown] + args: ['--format', 'brief'] diff --git a/.tflint.hcl b/.tflint.hcl index ab0b12a..1ae8876 100644 --- a/.tflint.hcl +++ b/.tflint.hcl @@ -48,3 +48,7 @@ rule "terraform_required_providers" { rule "terraform_standard_module_structure" { enabled = true } + +rule "aws_instance_invalid_type" { + enabled = true +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..4b3cdc2 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,277 @@ +# Contributing to terraform-aws-backup + +Thank you for your interest in contributing to the terraform-aws-backup module! This document outlines our coding standards, development workflow, and review process. + +## ๐Ÿš€ Quick Start + +1. Fork the repository +2. Create a feature branch: `git checkout -b feature/your-feature-name` +3. Make your changes following our coding standards +4. Run pre-commit hooks: `pre-commit run --all-files` +5. Test your changes with examples +6. Submit a pull request + +## ๐Ÿ“‹ Code Quality Standards + +### Terraform Code Style + +#### Variable Definitions +- **Descriptive names**: Use clear, descriptive variable names +- **Consistent types**: Always specify variable types explicitly +- **Validation rules**: Add validation for all input variables where applicable +- **Documentation**: Include helpful descriptions with examples + +```hcl +# โœ… Good +variable "vault_kms_key_arn" { + description = "The server-side encryption key that is used to protect your backups" + type = string + default = null + + validation { + condition = var.vault_kms_key_arn == null ? true : ( + can(regex("^arn:aws:kms:", var.vault_kms_key_arn)) && + !can(regex("alias/aws/", var.vault_kms_key_arn)) + ) + error_message = "The vault_kms_key_arn must be a valid customer-managed KMS key ARN." + } +} + +# โŒ Bad +variable "key" { + type = string +} +``` + +#### Resource Organization +- **Logical grouping**: Group related resources together +- **Clear naming**: Use descriptive resource names +- **Conditional creation**: Use locals for complex conditions +- **Documentation**: Comment complex logic + +```hcl +# โœ… Good +locals { + should_create_vault = var.enabled && var.vault_name != null + should_create_lock = local.should_create_vault && var.locked +} + +resource "aws_backup_vault" "ab_vault" { + count = local.should_create_vault ? 1 : 0 + # ... +} + +# โŒ Bad +resource "aws_backup_vault" "ab_vault" { + count = var.enabled && var.vault_name != null ? 1 : 0 + # ... +} +``` + +#### Error Messages +- **Contextual information**: Include current values and guidance +- **Clear language**: Use simple, direct language +- **Helpful guidance**: Explain what the user should do + +```hcl +# โœ… Good +error_message = "changeable_for_days must be between 3 and 365 days. Current value: ${var.changeable_for_days}. This parameter controls the vault lock compliance period." + +# โŒ Bad +error_message = "Invalid value." +``` + +### Constants and Magic Numbers +- **No magic numbers**: Replace hardcoded values with named constants +- **Configurable defaults**: Make defaults configurable via variables +- **Clear naming**: Use descriptive names for constants + +```hcl +# โœ… Good +variable "default_lifecycle_delete_after_days" { + description = "Default number of days after creation that a recovery point is deleted" + type = number + default = 90 +} + +delete_after = try(lifecycle.value.delete_after, var.default_lifecycle_delete_after_days) + +# โŒ Bad +delete_after = try(lifecycle.value.delete_after, 90) +``` + +## ๐Ÿ”’ Security Standards + +### Input Validation +- **Validate all inputs**: Use validation blocks for all variables +- **Prevent misuse**: Block common misconfigurations +- **Security-first defaults**: Choose secure defaults + +### Sensitive Data +- **No hardcoded secrets**: Never commit secrets or keys +- **Secure defaults**: Use customer-managed keys over AWS-managed +- **Minimal permissions**: Follow principle of least privilege + +## ๐Ÿงช Testing Requirements + +### Before Submitting +1. **Linting**: All linting rules must pass + ```bash + terraform fmt -check -recursive + tflint --config=.tflint.hcl + ``` + +2. **Security scanning**: Checkov security checks must pass + ```bash + checkov -d . --framework terraform + ``` + +3. **Examples**: Test all relevant examples + ```bash + cd examples/simple_plan + terraform init + terraform plan + ``` + +4. **Backwards compatibility**: Verify no breaking changes + ```bash + # Before changes + terraform plan -out=before.plan + # After changes + terraform plan -out=after.plan + # Compare plans should show identical resources + ``` + +## ๐Ÿ“ Code Review Checklist + +### For Reviewers + +#### โœ… Code Quality +- [ ] Code follows Terraform best practices +- [ ] Variables have proper validation and documentation +- [ ] No magic numbers or hardcoded values +- [ ] Error messages are helpful and contextual +- [ ] Complex logic is simplified and well-commented + +#### โœ… Security +- [ ] No secrets or sensitive data in code +- [ ] Input validation prevents common misconfigurations +- [ ] Security scanning (Checkov) passes +- [ ] Uses secure defaults (customer-managed keys, etc.) + +#### โœ… Compatibility +- [ ] Changes are backwards compatible +- [ ] All existing examples still work +- [ ] terraform plan produces identical results for existing configurations +- [ ] No breaking changes to variable or output interfaces + +#### โœ… Documentation +- [ ] README updated if needed +- [ ] Variable descriptions are clear and helpful +- [ ] Examples demonstrate new features +- [ ] CHANGELOG updated for user-facing changes + +#### โœ… Testing +- [ ] All linting checks pass +- [ ] Security scans pass +- [ ] Examples work correctly +- [ ] terraform-docs generates correct documentation + +### For Contributors + +#### Before Submitting PR +- [ ] Followed coding standards outlined above +- [ ] Added/updated tests for new functionality +- [ ] Updated documentation as needed +- [ ] Ran pre-commit hooks successfully +- [ ] Tested examples work correctly +- [ ] Verified backwards compatibility + +#### PR Description Should Include +- [ ] Clear description of changes +- [ ] Reasoning for the changes +- [ ] Any breaking changes (should be rare) +- [ ] Testing performed +- [ ] Screenshots/examples if applicable + +## ๐Ÿ”ง Development Environment Setup + +### Prerequisites +- Terraform >= 1.0 +- Pre-commit hooks +- tflint with AWS ruleset +- Checkov + +### Setup +```bash +# Install pre-commit +pip install pre-commit + +# Install hooks +pre-commit install + +# Install tflint +curl -s https://raw.githubusercontent.com/terraform-linters/tflint/master/install_linux.sh | bash + +# Install tflint AWS ruleset +tflint --init + +# Install checkov +pip install checkov +``` + +### Pre-commit Configuration +Our pre-commit configuration includes: +- Terraform formatting (`terraform fmt`) +- Terraform validation (`terraform validate`) +- Terraform documentation (`terraform-docs`) +- Terraform linting (`tflint`) +- Security scanning (`checkov`) +- Secrets detection (`detect-secrets`) +- Spell checking for documentation +- General file quality checks + +## ๐ŸŽฏ Contribution Guidelines + +### Types of Contributions +- **Bug fixes**: Always welcome +- **Feature enhancements**: Discuss in issues first +- **Documentation improvements**: Very helpful +- **Example additions**: Great for community + +### Breaking Changes +- **Avoid when possible**: Strive for backwards compatibility +- **Major version only**: Breaking changes only in major releases +- **Clear migration path**: Provide migration guide +- **Advance notice**: Discuss in issues before implementing + +### Code Organization +- **Maintain standard structure**: Keep standard Terraform file layout +- **Logical grouping**: Group related functionality together +- **Clear separation**: Separate concerns appropriately +- **Consistent patterns**: Follow existing code patterns + +## ๐Ÿท๏ธ Versioning and Releases + +We follow [Semantic Versioning](https://semver.org/): +- **MAJOR**: Breaking changes +- **MINOR**: New features (backwards compatible) +- **PATCH**: Bug fixes + +## ๐Ÿค Community + +- **Be respectful**: Follow our code of conduct +- **Be collaborative**: Help others learn and contribute +- **Be constructive**: Provide helpful feedback +- **Be patient**: Reviews take time for quality + +## ๐Ÿ“š Additional Resources + +- [Terraform Best Practices](https://www.terraform-best-practices.com/) +- [AWS Backup Documentation](https://docs.aws.amazon.com/backup/) +- [Module Examples](./examples/) +- [Issue Templates](./.github/ISSUE_TEMPLATE/) + +--- + +Thank you for contributing to terraform-aws-backup! Your contributions help make AWS backup management easier for everyone. \ No newline at end of file diff --git a/main.tf b/main.tf index 7222bbe..c3e21a1 100644 --- a/main.tf +++ b/main.tf @@ -1,8 +1,67 @@ +# Organized locals for better maintainability and code clarity +locals { + # Resource creation conditions + should_create_vault = var.enabled && var.vault_name != null + should_create_lock = local.should_create_vault && var.locked + should_create_legacy_plan = var.enabled && length(var.plans) == 0 && length(local.rules) > 0 + + # Validation helpers for vault lock configuration + vault_lock_requirements_met = var.min_retention_days != null && var.max_retention_days != null + retention_days_valid = local.vault_lock_requirements_met ? var.min_retention_days <= var.max_retention_days : true + check_retention_days = var.locked ? (local.vault_lock_requirements_met && local.retention_days_valid) : true + + # Rule processing (matching existing logic for compatibility) + rule = var.rule_name == null ? [] : [{ + name = var.rule_name + target_vault_name = var.vault_name != null ? var.vault_name : "Default" + schedule = var.rule_schedule + start_window = var.rule_start_window + completion_window = var.rule_completion_window + lifecycle = var.rule_lifecycle_cold_storage_after == null ? {} : { + cold_storage_after = var.rule_lifecycle_cold_storage_after + delete_after = var.rule_lifecycle_delete_after + } + enable_continuous_backup = var.rule_enable_continuous_backup + recovery_point_tags = var.rule_recovery_point_tags + }] + + rules = concat(local.rule, var.rules) + + # Selection processing (comprehensive logic for VSS validation) + selection_resources = flatten([ + # Legacy single selection + var.selection_resources, + # Legacy multiple selections (var.selections) + [for selection in try(tolist(var.selections), []) : try(selection.resources, [])], + [for k, selection in try(tomap(var.selections), {}) : try(selection.resources, [])], + # New multiple selections (var.backup_selections) + [for selection in var.backup_selections : try(selection.resources, [])], + # Plan-based selections + [for plan in var.plans : flatten([for selection in try(plan.selections, []) : try(selection.resources, [])])] + ]) + + # Plans processing + plans_map = var.plans + + # Lifecycle validations + lifecycle_validations = alltrue([ + for rule in local.rules : ( + length(try(rule.lifecycle, {})) == 0 ? true : + try(rule.lifecycle.cold_storage_after, var.default_lifecycle_cold_storage_after_days) <= try(rule.lifecycle.delete_after, var.default_lifecycle_delete_after_days) + ) && + alltrue([ + for copy_action in try(rule.copy_actions, []) : ( + length(try(copy_action.lifecycle, {})) == 0 ? true : + try(copy_action.lifecycle.cold_storage_after, var.default_lifecycle_cold_storage_after_days) <= try(copy_action.lifecycle.delete_after, var.default_lifecycle_delete_after_days) + ) + ]) + ]) +} # AWS Backup vault resource "aws_backup_vault" "ab_vault" { - count = var.enabled && var.vault_name != null ? 1 : 0 + count = local.should_create_vault ? 1 : 0 name = var.vault_name kms_key_arn = var.vault_kms_key_arn @@ -12,7 +71,7 @@ resource "aws_backup_vault" "ab_vault" { # AWS Backup vault lock configuration resource "aws_backup_vault_lock_configuration" "ab_vault_lock_configuration" { - count = var.enabled && var.vault_name != null && var.locked ? 1 : 0 + count = local.should_create_lock ? 1 : 0 backup_vault_name = aws_backup_vault.ab_vault[0].name min_retention_days = var.min_retention_days @@ -29,7 +88,7 @@ resource "aws_backup_vault_lock_configuration" "ab_vault_lock_configuration" { # Legacy AWS Backup plan (for backward compatibility) resource "aws_backup_plan" "ab_plan" { - count = var.enabled && length(var.plans) == 0 && length(local.rules) > 0 ? 1 : 0 + count = local.should_create_legacy_plan ? 1 : 0 name = coalesce(var.plan_name, "aws-backup-plan-${var.vault_name != null ? var.vault_name : "default"}") # Rules @@ -48,8 +107,8 @@ resource "aws_backup_plan" "ab_plan" { dynamic "lifecycle" { for_each = length(try(rule.value.lifecycle, {})) == 0 ? [] : [rule.value.lifecycle] content { - cold_storage_after = try(lifecycle.value.cold_storage_after, 0) - delete_after = try(lifecycle.value.delete_after, 90) + cold_storage_after = try(lifecycle.value.cold_storage_after, var.default_lifecycle_cold_storage_after_days) + delete_after = try(lifecycle.value.delete_after, var.default_lifecycle_delete_after_days) } } @@ -63,8 +122,8 @@ resource "aws_backup_plan" "ab_plan" { dynamic "lifecycle" { for_each = length(try(copy_action.value.lifecycle, {})) == 0 ? [] : [copy_action.value.lifecycle] content { - cold_storage_after = try(lifecycle.value.cold_storage_after, 0) - delete_after = try(lifecycle.value.delete_after, 90) + cold_storage_after = try(lifecycle.value.cold_storage_after, var.default_lifecycle_cold_storage_after_days) + delete_after = try(lifecycle.value.delete_after, var.default_lifecycle_delete_after_days) } } } @@ -124,8 +183,8 @@ resource "aws_backup_plan" "ab_plans" { dynamic "lifecycle" { for_each = length(try(rule.value.lifecycle, {})) == 0 ? [] : [rule.value.lifecycle] content { - cold_storage_after = try(lifecycle.value.cold_storage_after, 0) - delete_after = try(lifecycle.value.delete_after, 90) + cold_storage_after = try(lifecycle.value.cold_storage_after, var.default_lifecycle_cold_storage_after_days) + delete_after = try(lifecycle.value.delete_after, var.default_lifecycle_delete_after_days) } } @@ -139,8 +198,8 @@ resource "aws_backup_plan" "ab_plans" { dynamic "lifecycle" { for_each = length(try(copy_action.value.lifecycle, {})) == 0 ? [] : [copy_action.value.lifecycle] content { - cold_storage_after = try(lifecycle.value.cold_storage_after, 0) - delete_after = try(lifecycle.value.delete_after, 90) + cold_storage_after = try(lifecycle.value.cold_storage_after, var.default_lifecycle_cold_storage_after_days) + delete_after = try(lifecycle.value.delete_after, var.default_lifecycle_delete_after_days) } } } @@ -173,63 +232,3 @@ resource "aws_backup_plan" "ab_plans" { } } -locals { - # Rule - rule = var.rule_name == null ? [] : [ - { - name = var.rule_name - target_vault_name = var.vault_name != null ? var.vault_name : "Default" - schedule = var.rule_schedule - start_window = var.rule_start_window - completion_window = var.rule_completion_window - lifecycle = var.rule_lifecycle_cold_storage_after == null ? {} : { - cold_storage_after = var.rule_lifecycle_cold_storage_after - delete_after = var.rule_lifecycle_delete_after - } - enable_continuous_backup = var.rule_enable_continuous_backup - recovery_point_tags = var.rule_recovery_point_tags - } - ] - - # Rules - rules = concat(local.rule, var.rules) - - # Plans map for multiple plans - plans_map = var.plans - - # Helper for VSS validation - collect resources from all selection sources - selection_resources = flatten([ - # Legacy single selection - var.selection_resources, - # Legacy multiple selections (var.selections) - [for selection in try(tolist(var.selections), []) : try(selection.resources, [])], - [for k, selection in try(tomap(var.selections), {}) : try(selection.resources, [])], - # New multiple selections (var.backup_selections) - [for selection in var.backup_selections : try(selection.resources, [])], - # Plan-based selections - [for plan in var.plans : flatten([for selection in try(plan.selections, []) : try(selection.resources, [])])] - ]) - - # Lifecycle validations - lifecycle_validations = alltrue([ - for rule in local.rules : ( - length(try(rule.lifecycle, {})) == 0 ? true : - try(rule.lifecycle.cold_storage_after, 0) <= try(rule.lifecycle.delete_after, 90) - ) && - alltrue([ - for copy_action in try(rule.copy_actions, []) : ( - length(try(copy_action.lifecycle, {})) == 0 ? true : - try(copy_action.lifecycle.cold_storage_after, 0) <= try(copy_action.lifecycle.delete_after, 90) - ) - ]) - ]) - - # Check retention days - handling null values properly - check_retention_days = var.locked ? ( - var.min_retention_days == null ? false : ( - var.max_retention_days == null ? false : ( - var.min_retention_days <= var.max_retention_days - ) - ) - ) : true -} diff --git a/variables.tf b/variables.tf index c230e47..c3ce664 100644 --- a/variables.tf +++ b/variables.tf @@ -57,7 +57,7 @@ variable "changeable_for_days" { validation { condition = var.changeable_for_days == null ? true : var.changeable_for_days >= 3 && var.changeable_for_days <= 365 - error_message = "The changeable_for_days must be between 3 and 365 days." + error_message = "changeable_for_days must be between 3 and 365 days. This parameter controls the vault lock compliance period - the number of days before the lock becomes immutable." } } @@ -150,7 +150,7 @@ variable "rule_schedule" { validation { condition = var.rule_schedule == null ? true : ( can(regex("^rate\\(", var.rule_schedule)) ? - !can(regex("rate\\([1-9] minute[^s]", var.rule_schedule)) : true + !can(regex("rate\\(([1-9]|1[0-4])\\s+minutes?\\)", var.rule_schedule)) : true ) error_message = "Rate expressions should not be more frequent than every 15 minutes for backup operations. Use 'rate(15 minutes)' or higher intervals." } @@ -459,7 +459,7 @@ variable "backup_policies" { condition = alltrue([ for policy in var.backup_policies : can(regex("^rate\\(", policy.schedule)) ? - !can(regex("rate\\([1-9] minute[^s]", policy.schedule)) : true + !can(regex("rate\\(([1-9]|1[0-4])\\s+minutes?\\)", policy.schedule)) : true ]) error_message = "Rate expressions should not be more frequent than every 15 minutes for backup operations. Use 'rate(15 minutes)' or higher intervals." } @@ -530,3 +530,28 @@ variable "backup_regions" { type = list(string) default = [] } + +# +# Default lifecycle configuration constants +# +variable "default_lifecycle_delete_after_days" { + description = "Default number of days after creation that a recovery point is deleted. Used when delete_after is not specified in lifecycle configuration." + type = number + default = 90 + + validation { + condition = var.default_lifecycle_delete_after_days >= 1 + error_message = "The default_lifecycle_delete_after_days must be at least 1 day." + } +} + +variable "default_lifecycle_cold_storage_after_days" { + description = "Default number of days after creation that a recovery point is moved to cold storage. Used when cold_storage_after is not specified in lifecycle configuration." + type = number + default = 0 + + validation { + condition = var.default_lifecycle_cold_storage_after_days == 0 || var.default_lifecycle_cold_storage_after_days >= 30 + error_message = "The default_lifecycle_cold_storage_after_days must be 0 (disabled) or at least 30 days (AWS minimum requirement)." + } +}