Automating Cross-Region Backups in AWS with Terraform

When managing data heavy infrastructure at scale we often forget about backups as we are used to fact that cloud will not fail us until one day you actually need to restore it. Having a consistent, automated backup process across environments is one of the simplest ways to ensure data durability, compliance, and disaster recovery readiness.

In this post, I’ll show how to Automate AWS Backup with Terraform. The module we’ll review supports cross-region backups, lifecycle policies (moving data to cold storage or deleting after a set time), and integrates with KMS for encryption. It’s designed to protect critical data services like DynamoDB and RDS without manual effort. Nevertheless you can backup a lot of different services listed here.

Automate Backups with Terraform


If you’ve ever relied on manual backups and snapshotting, you know how easy it is for things to drift out of sync as some parts are backed up, others missed, inconsistent retention across environments. Additionally it is difficult to maintain backups and their configurations over time, especially as new resources are added.

By managing backups as code, we ensure:

  • Consistency – same retention and encryption rules everywhere
  • Security – automatic encryption using KMS keys
  • Resilience – cross-region copies for disaster recovery
  • Governance – policies can’t be skipped or forgotten

Terraform becomes the single source of truth for not just creating infrastructure, but preserving it, therefore we can create Terraform module that can be called whenever we need backup configuration.

How the module works

The core idea is to create:

  1. An IAM role that AWS Backup assumes
  2. A Backup Vault to store backups (encrypted with a KMS key)
  3. A Backup Plan that defines schedule, lifecycle, and copy rules
  4. A Selection that specifies which resources to back up
  5. A Cross-Region Vault for replication
IAM Role

Since AWS Backup is managed service so it needs permissions to access resources when creating and restoring backups. The module defines a role and attaches the required policies:

# IAM role for AWS Backup
data "aws_partition" "current" {}

data "aws_iam_policy_document" "assume_role" {
  statement {
    actions = ["sts:AssumeRole"]
    effect  = "Allow"
    principals {
      type        = "Service"
      identifiers = ["backup.amazonaws.com"]
    }
  }
}

resource "aws_iam_role" "backup_role" {
  name               = "${var.vault_name}-role"
  assume_role_policy = data.aws_iam_policy_document.assume_role.json
}

# Attach required AWS managed policies
resource "aws_iam_role_policy_attachment" "backup_policies" {
  for_each = toset([
    "arn:${data.aws_partition.current.partition}:iam::aws:policy/service-role/AWSBackupServiceRolePolicyForBackup",
    "arn:${data.aws_partition.current.partition}:iam::aws:policy/AWSBackupServiceRolePolicyForS3Backup",
    "arn:${data.aws_partition.current.partition}:iam::aws:policy/service-role/AWSBackupServiceRolePolicyForRestores",
    "arn:${data.aws_partition.current.partition}:iam::aws:policy/AWSBackupServiceRolePolicyForS3Restore"
  ])

  role       = aws_iam_role.backup_role.name
  policy_arn = each.value
}

Data block aws_partition within module itself to retrieve dynamically provider context and use it within IAM policy like this data.aws_partition.current.partition.
In this example, our AWS Backup IAM Role is granted a set of managed policies that let the Backup service perform its job:

As you might noticed S3 permissions for Backup are not included within first managed policy as S3 backups were introduced later and first policy includes services like: DynamoDB, RDS, EBS, EFS, Redshift, Timestream and many more.

Backup Vault and Plan
data "aws_kms_key" "this" {
  key_id = var.kms_key_arn
}

resource "aws_backup_vault" "this" {
  name        = var.vault_name
  kms_key_arn = data.aws_kms_key.this.arn
}

#Cross-Region backup
provider "aws" {
  alias  = "backup"
  region = "eu-west-1"
}

resource "aws_backup_vault" "backup" {
  provider    = aws.backup
  name        = var.vault_name
  kms_key_arn = data.aws_kms_key.backup.arn
}

data "aws_kms_key" "backup" {
  provider = aws.backup
  key_id   = var.kms_key_arn_backup
}

This resource defines the Backup Vault, which acts as a secure container for all your backup recovery points. Each vault is tied to a KMS key for encryption ensuring all backups are protected at rest. You can setup multiple vaults per env or region to further isolate backups and manage different retentions or policies.

Important to mention that in this setup we are using two vaults as one is for the primary region and another for cross-region replication. The backup plan includes therefore two copy actions as you will see:

  • one to manage lifecycle transitions in the same region
  • one to manage to replicate backups to the secondary vault for disaster recovey



Then, we define a Backup Plan that schedules and manages retention:

resource "aws_backup_plan" "this" {
  name = "${var.vault_name}-backup-plan"

  rule {
    rule_name         = "${var.vault_name}-backup-rule"
    target_vault_name = aws_backup_vault.this.name
    schedule          = "cron(${var.backup_cron})"
    
    copy_action {
      destination_vault_arn = aws_backup_vault.this.arn
      lifecycle {
        delete_after       = var.delete_backup_after
        cold_storage_after = var.cold_storage_after
      }
    }
    copy_action {
      destination_vault_arn = aws_backup_vault.backup.arn
      lifecycle {
        delete_after       = var.delete_backup_after
        cold_storage_after = var.cold_storage_after
      }
    }

    lifecycle {
      delete_after       = var.delete_backup_after
      cold_storage_after = var.cold_storage_after
    }
  }

}

This is actually where cross-region setup is configured. It copies every backup to the secondary region vault using a different encryption key that is managed through the aws.backup provider alias. This alias allows the Terraform module use two provider configurations at once, so that Terraform knows exactly which resource belongs to which region and can manage cross-region backups cleanly.

Then we deploy selection resource which connects backup plan to the resource we want to be “protected”. It tells Backup which IAM role to use when running backup jobs which is the role we created previously. It links to the specific backup plan and accepts as input list of resources to include in the plan.

resource "aws_backup_selection" "aws_backup_selection" {
  iam_role_arn = aws_iam_role.backup_role.arn
  name         = "${var.vault_name}-backup-selection"
  plan_id      = aws_backup_plan.this.id
  resources    = var.resources
}

The selection only defines which resources to protect; cross-region replication is handled entirely by the backup plan.

Here’s how the module is called in Terraform:
module "test_backup" {

  source              = "../modules/backup"
  vault_name          = local.is_prod == false ? "${var.backup_vault.name}-backup-vault-${local.env}" : "${var.backup_vault.name}-backup-vault"
  delete_backup_after = var.backup_vault.retention_days
  cold_storage_after  = var.backup_vault.cold_storage_after
  backup_cron         = var.backup_vault.cron
  kms_key_arn         = aws_kms_key.this.arn
  kms_key_arn_backup  = aws_kms_key.ingestion_key_backup.arn
  resources = [
    aws_dynamodb_table.test_table.arn,
    aws_timestreamwrite_table.test_timestream_table.arn,
    aws_s3_bucket.test_bucket.arn
  ]
}

This module call deploys the AWS Backup configuration that automatically protects DynamoDB, Timestream and S3 resources. It adapts naming based on the environment, applies defined retention and cold storage policies, and replicates backups across regions using separate KMS keys for encryption.
An example of variable backup_vault could look like this:

variable "backup_vault" {
  description = "Backup vault properties"
  type = object({
    name               = string
    retention_days     = number
    cron               = string
    cold_storage_after = number
  })
  default = {
    name               = "test"
    retention_days     = 180
    cron               = "0 3 ? * MON *"
    cold_storage_after = 30
  }
}

This configuration is of type object as variable has different types of data and it keeps backups for 180 days, moves them to a cold storage after 30 days and schedules the backup every Monday at 03:00 UTC.

Lifecycle Policies: Save Storage, Not Just Data

AWS Backup allows lifecycle transitions — for example:

  • Move backups to cold storage after 30 days
  • Delete them after 180 days

Terraform makes this part of your infrastructure definition, ensuring retention policies are never forgotten or changed manually.

Cross-region backups are not just about redundancy, they’re about business continuity.
By encrypting data with separate regional KMS keys and automating replication, your organization satisfies common compliance frameworks of course if there are any. 😀

Automated backups

Since this module is part of your Terraform configuration, you have full flexibility in how and when to use it. If your project is split across multiple Terraform workspaces, you can define a separate configuration per workspace — and whenever new resources are added, simply include them in the module call and run terraform apply to automatically bring them under backup coverage.

Conclusion

Automating AWS Backup through Terraform provides more than convenience, it builds resilience into your architecture by default.
With a single module, you can:

  • Enforce encryption standards
  • Control lifecycle and retention
  • Achieve cross-region resilience
  • Simplify compliance audits

If this blog saved you time, support me with a coffee!

Thanks to everyone who’s supported!