AWS Backup Windows EC2 Instance – Part 1

The AWS Backup service allows you to create ‘point in time’ backups of specific AWS resources which you store in a primary backup vault. Backups in the primary vault can be replicated to another region for disaster recovery (DR). In this blog post series I will demonstrate how to:

  • Create a primary or source Backup Vault;
  • Create some Backups Plans (schedule, retention, lifecycle);
  • Target EC2 instances for backup via tags;
  • Install the VSS backup component;
  • Make the backups Windows VSS aware; and
  • Copy weekly backups to a DR vault in another region

In this demo we will use a single account with backup vaults in separate regions (Primary Backup Vault in Sydney & DR Backup Vault in Singapore). It is possible to enable AWS Backups at an organisation level and this is the recommended approach. It is also possible to copy backups to another account in another region. For a production deployment, I would also recommend putting the DR backup vault in a different account in another region to be more secure.

In part one, we create the base backup components across two regions. The components are:

  • Primary Backup Vault in Sydney
  • Backup Plans
  • Backup Selections
  • DR backup vault in Singapore
  • KMS keys for both vaults
  • Role service for AWS backup service

The code to deploy the base components above can be found in the Github repository here (https://github.com/arinzl/aws-monitoring-ec2-windows-part1). Note, the default AWS Terraform provider will only deploy to a single region, so we need to use multiple AWS Terraform providers to deploy to both regions from the same code source.

The AWS Backup provides the management plane for the service, and co-ordinates operation of the remaining components below:

Primary Backup Vault: A collection of backups in the primary region

Backup Plans: determines: what and when backups happen; which Backup Vault receives the backup; how long backups are retained in the Backup Vault and if backups are to be copied to another vault. These settings are managed via rules.

Backup Selection: Used to target EC2 instances to a backup plan (essentially the glue between Backup Plans and endpoint EC2 instances).

KMS: provides keys to encrypt the vault (which is data at rest)

Service role: used by AWS backup to create backups in the vault (and restore)

The Backup Plans can contain multiple rules where each rule determines:

  • Target backup vault
  • schedule (& start window)
  • maximum backup duration
  • lifecycle of backups (backup retention)
  • Additional recovery tags to add to backups
  • Whether to copy backups to another Backup Vault

If we wanted to backup an EC2 instance daily, weekly, monthly & quarterly, we can achieve this with 4 rules.

Note the AWS backup service will manage the storage (AMIs, snapshots, volumes etc) for the backups. Users will not be able to manage AWS backup objects such as AMI’s via the standard AWS console as these are managed by the AWS Backup service.

Summary of files in the Github repository for this blog:

awsbackup.tf – configuration of: primary backup vault, backup plans, backup selection, DR backup vault

#------------------------------------------------------------------------------
# Backup Vault - Primary region
#------------------------------------------------------------------------------

resource "aws_backup_vault" "backup_vault" {
  name        = "tutorial-backup-vault"
  kms_key_arn = aws_kms_key.backup_kms_key.arn

}

#------------------------------------------------------------------------------
# Backup Plans - VSS enabled
#------------------------------------------------------------------------------

resource "aws_backup_plan" "tier2-vss" {
  depends_on = [aws_backup_vault.backup_vault_dr]
  name       = "tier2-vss"

  rule {
    rule_name         = "daily-vss"
    target_vault_name = aws_backup_vault.backup_vault.id
    schedule          = "cron(0 14 * * ? *)"
    start_window      = "60"
    completion_window = "600"

    lifecycle {
      delete_after = 32
    }
    recovery_point_tags = {
      tier = "2VSS"
      rule = "daily-vss"
    }
  }

  rule {
    rule_name         = "weekly-vss"
    target_vault_name = aws_backup_vault.backup_vault.id
    schedule          = "cron(0 15 ? * SAT *)"
    start_window      = "60"
    completion_window = "600"

    lifecycle {
      delete_after = 57
    }
    recovery_point_tags = {
      tier = "2VSS"
      rule = "weekly-vss"
    }

    copy_action {
      lifecycle {
        delete_after = 8
      }
      destination_vault_arn = "arn:aws:backup:${var.dr-region}:${data.aws_caller_identity.current.account_id}:backup-vault:${aws_backup_vault.backup_vault_dr.id}"
    }
  }

  rule {
    rule_name         = "monthly-vss"
    target_vault_name = aws_backup_vault.backup_vault.id
    schedule          = "cron(0 16 3 * ? *)"
    start_window      = "60"
    completion_window = "600"

    lifecycle {
      delete_after = 365
    }
    recovery_point_tags = {
      tier = "2VSS"
      rule = "monthly-vss"
    }
  }

  rule {
    rule_name         = "quarterly-vss"
    target_vault_name = aws_backup_vault.backup_vault.id
    schedule          = "cron(0 17 3 JAN,APR,JUL,OCT ? *)"
    start_window      = "60"
    completion_window = "600"

    lifecycle {
      delete_after = 2555
    }
    recovery_point_tags = {
      tier = "2"
      rule = "quarterly-vss"
    }
  }

  advanced_backup_setting {
    backup_options = {
      WindowsVSS = "enabled"
    }
    resource_type = "EC2"
  }
}


resource "aws_backup_plan" "tier3-vss" {
  name = "tier3-vss"

  rule {
    rule_name         = "daily-vss"
    target_vault_name = aws_backup_vault.backup_vault.id
    schedule          = "cron(0 14 * * ? *)"
    start_window      = "60"
    completion_window = "600"

    lifecycle {
      delete_after = 8
    }
    recovery_point_tags = {
      tier = "3VSS"
      rule = "daily-vss"
    }
  }

  rule {
    rule_name         = "weekly-vss"
    target_vault_name = aws_backup_vault.backup_vault.id
    schedule          = "cron(0 15 ? * SAT *)"
    start_window      = "60"
    completion_window = "600"

    lifecycle {
      delete_after = 32
    }
    recovery_point_tags = {
      tier = "3VSS"
      rule = "weekly-vss"
    }
  }

  rule {
    rule_name         = "monthly-vss"
    target_vault_name = aws_backup_vault.backup_vault.id
    schedule          = "cron(0 16 3 * ? *)"
    start_window      = "60"
    completion_window = "600"

    lifecycle {
      delete_after = 93
    }
    recovery_point_tags = {
      tier = "3VSS"
      rule = "monthly-vss"
    }
  }

  rule {
    rule_name         = "quarterly-vss"
    target_vault_name = aws_backup_vault.backup_vault.id
    schedule          = "cron(0 17 3 JAN,APR,JUL,OCT ? *)"
    start_window      = "60"
    completion_window = "600"

    lifecycle {
      delete_after = 365
    }
    recovery_point_tags = {
      tier = "3VSS"
      rule = "quarterly-vss"
    }
  }

  advanced_backup_setting {
    backup_options = {
      WindowsVSS = "enabled"
    }
    resource_type = "EC2"
  }
}


#------------------------------------------------------------------------------
# Backup Selection - VSS
#------------------------------------------------------------------------------

resource "aws_backup_selection" "tier2-vss" {
  iam_role_arn = module.backup_iam_assumable_role.iam_role_arn
  name         = "tier2-vss-selection"
  plan_id      = aws_backup_plan.tier2-vss.id

  selection_tag {
    type  = "STRINGEQUALS"
    key   = "backuptier"
    value = "2VSS"
  }

}

resource "aws_backup_selection" "tier3-vss" {
  iam_role_arn = module.backup_iam_assumable_role.iam_role_arn
  name         = "tier3-vss-selection"
  plan_id      = aws_backup_plan.tier3-vss.id

  selection_tag {
    type  = "STRINGEQUALS"
    key   = "backuptier"
    value = "3VSS"
  }

}

#------------------------------------------------------------------------------
# DR Region Resources
#------------------------------------------------------------------------------

#------------------------------------------------------------------------------
# Backup Vault - DR Region
#------------------------------------------------------------------------------
resource "aws_backup_vault" "backup_vault_dr" {
  provider = aws.dr-region

  name        = "tutorial-backup-vault-dr"
  kms_key_arn = aws_kms_key.backup_dr_kms_key.arn

}
}

data.tf – Used to discover the target AWS account ID programmatically. IAM policy document to allow the backup service to pass a role for restore operations.

data "aws_caller_identity" "current" {}


data "aws_iam_policy_document" "backuprestore_passrole_doc" {
  statement {
    sid    = "BackupRestorePermissions1"
    effect = "Allow"
    actions = [
      "iam:PassRole"
    ]
    resources = ["arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/*",
    ]
  }
}

iam.tf – Role service for AWS Backup Service to assume

#------------------------------------------------------------------------------
# IAM Roles
#------------------------------------------------------------------------------

module "backup_iam_assumable_role" {
  source  = "terraform-aws-modules/iam/aws//modules/iam-assumable-role"
  version = "~> 4.5"

  trusted_role_services = [
    "backup.amazonaws.com"
  ]

  role_requires_mfa       = false
  create_role             = true
  create_instance_profile = true

  role_name = "tutorial-aws-backup-role-service"

  custom_role_policy_arns = [
    "arn:aws:iam::aws:policy/service-role/AWSBackupServiceRolePolicyForBackup",
    "arn:aws:iam::aws:policy/service-role/AWSBackupServiceRolePolicyForRestores"
  ]
}

resource "aws_iam_policy" "backuprestore_passrole_policy" {
  name   = "${var.ec2_app_name}-ec2-restore-passrole-policy"
  path   = "/"
  policy = data.aws_iam_policy_document.backuprestore_passrole_doc.json

}

resource "aws_iam_role_policy_attachment" "backuprestore_passrole_policy_attachement" {
  role       = module.backup_iam_assumable_role.iam_role_name
  policy_arn = aws_iam_policy.backuprestore_passrole_policy.arn
}

kms.tf – creates KMS key (to encrypt Backup Vault at rest) and KMS alias for each region

#------------------------------------------------------------------------------
# KMS Key for Backup Vault - Primary location
#------------------------------------------------------------------------------

resource "aws_kms_key" "backup_kms_key" {
  #checkov:skip=CKV_AWS_33: Ensure KMS key policy does not contain wildcard (*) principal 
  description         = "KMS Keys for Backup Encryption"
  is_enabled          = true
  enable_key_rotation = true

  tags = merge(local.tags_generic, local.tag_backup)


  policy = <<EOF
{
    "Version": "2012-10-17",
    "Id": "primary-region-backup",
    "Statement": [
        {
            "Sid": "Enable IAM User Administration Permissions",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"
            },
            "Action": "kms:*",
            "Resource": "*"
        },
        {
            "Sid": "Allow access through Backup for all principals in the account that are authorized to use Backup Storage",
            "Effect": "Allow",
            "Principal": {
                "AWS": "*"
            },
            "Action": [
                "kms:CreateGrant",
                "kms:Decrypt",
                "kms:GenerateDataKey*"
            ],
            "Resource": "*",
            "Condition": {
                "StringEquals": {
                    "kms:ViaService": "backup.ap-southeast-2.amazonaws.com",
                    "kms:CallerAccount": "${data.aws_caller_identity.current.account_id}"
                }
            }
        },
        {
            "Sid": "Allow direct access to key metadata to the account",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"
            },
            "Action": [
                "kms:Describe*",
                "kms:Get*",
                "kms:List*",
                "kms:RevokeGrant"
            ],
            "Resource": "*"
        }
    ]
}
EOF
}

resource "aws_kms_alias" "backup_kms_alias" {
  target_key_id = aws_kms_key.backup_kms_key.key_id
  name          = "alias/tutorial-backup-primary-vault-key"
}


#------------------------------------------------------------------------------
# KMS Key for Backup Vault - DR location
#------------------------------------------------------------------------------

resource "aws_kms_key" "backup_dr_kms_key" {
  #checkov:skip=CKV_AWS_33: Ensure KMS key policy does not contain wildcard (*) principal 
  provider = aws.dr-region

  description         = "KMS Keys for DR Backup Vault Encryption"
  is_enabled          = true
  enable_key_rotation = true

  tags = merge(local.tags_generic, local.tag_backup)

  policy = <<EOF
{
    "Version": "2012-10-17",
    "Id": "dr-region-backup",
    "Statement": [
        {
            "Sid": "Enable IAM User Administration Permissions",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"
            },
            "Action": "kms:*",
            "Resource": "*"
        },
        {
            "Sid": "Allow access through Backup for all principals in the account that are authorized to use Backup Storage",
            "Effect": "Allow",
            "Principal": {
                "AWS": "*"
            },
            "Action": [
                "kms:CreateGrant",
                "kms:Decrypt",
                "kms:GenerateDataKey*"
            ],
            "Resource": "*",
            "Condition": {
                "StringEquals": {
                    "kms:ViaService": "backup.ap-southeast-2.amazonaws.com",
                    "kms:CallerAccount": "${data.aws_caller_identity.current.account_id}"
                }
            }
        },
        {
            "Sid": "Allow direct access to key metadata to the account",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"
            },
            "Action": [
                "kms:Describe*",
                "kms:Get*",
                "kms:List*",
                "kms:RevokeGrant"
            ],
            "Resource": "*"
        }
    ]
}
EOF
}

resource "aws_kms_alias" "backup_dr_kms_alias" {
  provider = aws.dr-region

  target_key_id = aws_kms_key.backup_dr_kms_key.key_id
  name          = "alias/tutorial-backup-dr-vault-key"
}

locals.tf – local settings and repeatable configuration blocks

locals {

  tags_generic = {
    environment = var.environment
    costcentre  = "TBC"
    ManagedBy   = var.ManagedByLocation
  }

  tag_backup = {
    Application = "Tutorial backup Primary"
  }

  tags_ec2 = {
    appname    = var.ec2_app_name
    backuptier = "2VSS"
  }

  tags_ssm_ssm = {
    Name = "myvpc-vpce-interface-ssm-ssm"
  }

  tags_ssm_ssmmessages = {
    Name = "myvpc-vpce-interface-ssm-ssmmessages"
  }

  tags_ssm_ec2messages = {
    Name = "myvpc-vpce-interface-ssm-ec2messages"
  }

  user_data_prod = <<-EOT
<powershell>
Set-TimeZone -Name "New Zealand Standard Time"
New-Item -Path "c:\temp" -Name "logfiles" -ItemType "directory"

## VSS Backup component install
c:
cd \temp
$Instance_ID = (Invoke-WebRequest -Uri http://169.254.169.254/latest/meta-data/instance-id -UseBasicParsing).Content
$Instance_ID > c:\temp\test.txt
Send-SSMCommand -DocumentName AWS-ConfigureAWSPackage -InstanceId $Instance_ID -Parameter @{'action'='Install';'name'='AwsVssComponents'} -Comment $Instance_ID > c:\temp\awsvsscomponents.txt
Start-Sleep -Seconds 30

</powershell>
  EOT
}

provider.tf – default Terraform file, provider for both regions

provider "aws" {
  region = var.region

  default_tags {
    tags = local.tags_generic
  }
}


provider "aws" {
  alias  = "dr-region"
  region = var.region_dr


}

terraform.tfvars – default Terraform file, this is where we set the region (ap-southeast-1 for Singapore). Note – we only need the first three variables for part 1.

region            = "ap-southeast-2"
environment       = "prod"
ManagedByLocation = "https://github.com"

backup-dr-vault-name = "tutorial-backup-vault-dr"
dr-region            = "ap-southeast-1"

vpc_cidr_range       = "172.17.0.0/20"
private_subnets_list = ["172.17.0.0/24"]
public_subnets_list  = ["172.17.3.0/24"]

ec2_app_name = "tutorial-backup"

region_dr               = "ap-southeast-1"
vpc_cidr_range_dr       = "172.17.16.0/20"
private_subnets_dr_list = ["172.17.16.0/24"]
public_subnets_dr_list  = ["172.17.19.0/24"]

variables.tf – default Terraform file, variable definitions. Note some variables will be set in part 2.

#------------------------------------------------------------------------------
# General
#------------------------------------------------------------------------------
variable "region" {
  description = "Primary region for deployment"
  type        = string
}


variable "environment" {
  description = "Organisation environment"
  type        = string
}

variable "ManagedByLocation" {
  description = "Location of Infrastructure of Code"
  type        = string
}


#------------------------------------------------------------------------------
# Backup Vault - DR
#------------------------------------------------------------------------------

variable "backup-dr-vault-name" {
  description = "DR vault name"
  type        = string
}

variable "dr-region" {
  description = "DR region name"
  type        = string
}

#------------------------------------------------------------------------------
# VPC
#------------------------------------------------------------------------------

variable "vpc_cidr_range" {
  type = string

}

variable "private_subnets_list" {
  description = "Private subnet list for infrastructure"
  type        = list(string)

}

variable "public_subnets_list" {
  description = "Public subnet list for infrastructure"
  type        = list(string)

}

variable "region_dr" {
  description = "DR region for deployment"
  type        = string
}

variable "vpc_cidr_range_dr" {
  type = string

}

variable "private_subnets_dr_list" {
  description = "Private subnet list for DR infrastructure"
  type        = list(string)

}

variable "public_subnets_dr_list" {
  description = "Private subnet list for DR infrastructure"
  type        = list(string)

}

#------------------------------------------------------------------------------
# EC2
#------------------------------------------------------------------------------

variable "ec2_app_name" {
  description = "Application running on EC2 instance"
  type        = string

}

versions.tf – Terraform file, setting minimum versions of providers

terraform {
  required_version = ">= 0.13.1"

  required_providers {
    aws = {
      version = ">= 3.74.0"
      source  = "hashicorp/aws"
    }
  }
}

The above code will allow you to conduct some test EC2 backups if you tag your EC2 instance with “backuptier : 2VSS” and install the AWS backup agent. A VSS backup gotcha is that the following instance types are not supported for VSS enabled backups (as they are small instances and might not take the backup successfully):

  • t3.nano
  • t3.micro
  • t3a.nano
  • t3a.micro
  • t2.nano
  • t2.micro

This is the end of part 1. In part 2 we will:

  • Deploy our EC2 instance;
  • Install the VSS backup component;
  • Tag EC2 instance appropriately to enable backup targeting; and
  • Add some Cloudwatch monitoring/alerts

One comment

Leave a comment