Creating Cross-Account DynamoDB Backups with Terraform
Tags: terraform, aws, dynamodb, security
Published: 2024-07-10 15:13:21

In my previous post, [Creating Secure Backups for DynamoDB Tables with Terraform](https://dev.to/sepiyush/creating-secure-backups-for-dynamodb-tables-with-terraform-4002), I discussed how to create DynamoDB table backups within the same AWS account. In this post, I will explain how to create cross-account DynamoDB backups, where the tables are in one AWS account and the backups are created in another AWS account for enhanced security and limited access. 

### Steps to Configure Cross-Account Backups

To achieve cross-account DynamoDB backups, follow these steps:

### Step 1: Create Destination Vault in Backup Account

First, set up the backup vault in the AWS account where you want to store the backups. This includes configuring a KMS key for encryption and setting up the vault policy to allow cross-account access.

```hcl
resource "aws_backup_vault" "destination_backup_vault" {
  name        = "destination-backup-vault"
  kms_key_arn = aws_kms_key.backup_vault_key.arn
}

resource "aws_kms_key" "backup_vault_key" {
  description         = "KMS key for backup vault encryption"
  enable_key_rotation = true
}

resource "aws_backup_vault_policy" "destination_backup_vault_policy" {
  backup_vault_name = aws_backup_vault.destination_backup_vault.name
  policy = jsonencode({
    Version = "2012-10-17",
    Statement = [
      {
        Sid : "AllowCrossAccountAccess",
        Effect = "Allow",
        Principal = {
          AWS = "arn:aws:iam::[YOUR SOURCE ACCOUNT ID]:root"
        },
        Action = [
          "backup:CopyIntoBackupVault"
        ],
        Resource = "*"
      }
    ]
  })
}
```

### Step 2: Create Source Vault in Source Account

Next, create the backup vault in the source account where the DynamoDB tables reside.

```hcl
resource "aws_backup_vault" "source_backup_vault" {
  name        = "source-backup-vault"
  kms_key_arn = aws_kms_key.backup_vault_key.arn
}

resource "aws_kms_key" "backup_vault_key" {
  description         = "KMS Key for Backup"
  enable_key_rotation = true
}
```

### Step 3: Create IAM Role with Relevant Permissions

Create an IAM role in the source account with the necessary permissions to perform backups, including the `AWSBackupServiceRolePolicyForBackup` policy.

```hcl
data "aws_iam_policy_document" "assume_role" {
  statement {
    effect = "Allow"

    principals {
      type        = "Service"
      identifiers = ["backup.amazonaws.com"]
    }

    actions = ["sts:AssumeRole"]
  }
}

resource "aws_iam_role" "source_backup_role" {
  name               = "source-backup-role"
  assume_role_policy = data.aws_iam_policy_document.assume_role.json

  inline_policy {
    name = "backup-policy"

    policy = jsonencode({
      Version = "2012-10-17",
      Statement = [
        {
          Effect = "Allow",
          Action = [
            "dynamodb:CreateBackup",
            "dynamodb:DeleteBackup",
            "dynamodb:DescribeBackup",
            "dynamodb:ListBackups",
            "dynamodb:ListTables",
            "dynamodb:RestoreTableFromBackup",
            "dynamodb:ListTagsOfResource",
            "dynamodb:StartAwsBackupJob",
            "dynamodb:RestoreTableFromAwsBackup"
          ],
          Resource = "*"
        },
        {
          Effect = "Allow",
          Action = [
            "backup:StartBackupJob",
            "backup:StopBackupJob",
            "backup:TagResource",
            "backup:UntagResource"
          ],
          Resource = "*"
        }
      ]
    })
  }
}

resource "aws_iam_role_policy_attachment" "service_backup_policy" {
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSBackupServiceRolePolicyForBackup"
  role       = aws_iam_role.source_backup_role.name
}
```

### Step 4: Create a Backup Plan

Configure a backup plan to periodically back up the DynamoDB tables and copy them to the destination vault.

```hcl
resource "aws_backup_plan" "cross_account_cross_region_copy" {
  name = "cross-account-cross-region-copy"

  rule {
    rule_name         = "copy-to-destination"
    target_vault_name = aws_backup_vault.source_backup_vault.name
    schedule          = "cron(38 13 * * ? *)"

    copy_action {
      destination_vault_arn = var.destination_backup_vault_arn
      lifecycle {
        delete_after = 30 # Retain for 30 days
      }
    }
  }
}

resource "aws_backup_selection" "dynamodb_cross_account_copy_selection" {
  plan_id      = aws_backup_plan.cross_account_cross_region_copy.id
  name         = "copy-backup-selection"
  iam_role_arn = aws_iam_role.source_backup_role.arn

  resources = [
    //table arns
  ]
}

resource "aws_backup_vault_policy" "source_backup_vault_policy" {
  backup_vault_name = aws_backup_vault.source_backup_vault.name
  policy = jsonencode({
    Version = "2012-10-17",
    Statement = [
      {
        Sid : "AllowCrossAccountAccess",
        Effect = "Allow",
        Principal = {
          AWS = "arn:aws:iam::[YOUR BACKUP ACCOUNT ID]:root"
        },
        Action = [
          "backup:CopyFromBackupVault",
          "backup:CopyIntoBackupVault"
        ],
        Resource = "*"
      }
    ]
  })
}
```

So your complete destination terraform file would look something like this 

```hcl
resource "aws_backup_vault" "source_backup_vault" {
  name        = "source-backup-vault"
  kms_key_arn = aws_kms_key.backup_vault_key.arn
}

resource "aws_kms_key" "backup_vault_key" {
  description         = "KMS Key for Backup"
  enable_key_rotation = true
}

resource "aws_backup_vault_policy" "source_backup_vault_policy" {
  backup_vault_name = aws_backup_vault.source_backup_vault.name
  policy = jsonencode({
    Version = "2012-10-17",
    Statement = [
      {
        Sid : "AllowCrossAccountAccess",
        Effect = "Allow",
        Principal = {
          AWS = "arn:aws:iam::[YOUR BACKUP ACCOUNT ID]:root"
        },
        Action = [
          "backup:CopyFromBackupVault",
          "backup:CopyIntoBackupVault"
        ],
        Resource = "*"
      }
    ]
  })
}

data "aws_iam_policy_document" "assume_role" {
  statement {
    effect = "Allow"

    principals {
      type        = "Service"
      identifiers = ["backup.amazonaws.com"]
    }

    actions = ["sts:AssumeRole"]
  }
}

resource "aws_iam_role" "source_backup_role" {
  name               = "source-backup-role"
  assume_role_policy = data.aws_iam_policy_document.assume_role.json

  inline_policy {
    name = "backup-policy"

    policy = jsonencode({
      Version = "2012-10-17",
      Statement = [
        {
          Effect = "Allow",
          Action = [
            "dynamodb:CreateBackup",
            "dynamodb:DeleteBackup",
            "dynamodb:DescribeBackup",
            "dynamodb:ListBackups",
            "dynamodb:ListTables",
            "dynamodb:RestoreTableFromBackup",
            "dynamodb:ListTagsOfResource",
            "dynamodb:StartAwsBackupJob",
            "dynamodb:RestoreTableFromAwsBackup"
          ],
          Resource = "*"
        },
        {
          Effect = "Allow",
          Action = [
            "backup:StartBackupJob",
            "backup:StopBackupJob",
            "backup:TagResource",
            "backup:UntagResource"
          ],
          Resource = "*"
        }
      ]
    })
  }
}

resource "aws_iam_role_policy_attachment" "service_backup_policy" {
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSBackupServiceRolePolicyForBackup"
  role       = aws_iam_role.source_backup_role.name
}

resource "aws_backup_plan" "cross_account_cross_region_copy" {
  name = "cross-account-cross-region-copy"

  rule {
    rule_name         = "copy-to-destination"
    target_vault_name = aws_backup_vault.source_backup_vault.name
    schedule          = "cron(38 13 * * ? *)"

    copy_action {
      destination_vault_arn = var.destination_backup_vault_arn
      lifecycle {
        delete_after = 30 # Retain for 30 days
      }
    }
  }
}

resource "aws_backup_selection" "dynamodb_cross_account_copy_selection" {
  plan_id      = aws_backup_plan.cross_account_cross_region_copy.id
  name         = "copy-backup-selection"
  iam_role_arn = aws_iam_role.source_backup_role.arn

  resources = [
    //table arns
  ]
}

```

### Understanding the Cron Schedule

The `schedule` parameter in the backup plan uses a cron expression to determine when the backup should run. Here is an explanation of the cron expression used in this configuration:

```hcl
schedule = "cron(38 13 * * ? *)"
```

- **38**: Minute (38th minute of the hour).
- **13**: Hour (1 PM UTC).
- **\***: Day of the month (any day).
- **\***: Month (any month).
- **?**: Day of the week (any day of the week).
- **\***: Year (optional, any year).

This configuration schedules the backup job to run daily at 1:38 PM UTC.

### Conclusion

Creating cross-account backups for DynamoDB tables enhances the security and availability of your data. By following the steps outlined in this guide, you can use Terraform to automate the process of setting up secure backups across different AWS accounts. This approach ensures that your backups are stored securely in a separate account, providing an additional layer of protection against data loss or unauthorized access.

If you have any suggestions or improvements, please feel free to comment. Happy Terraforming!