From 2ab8fd7fdbbe7fe7116efc62887ded749897d6bd Mon Sep 17 00:00:00 2001 From: Ian Ridehalgh Date: Mon, 24 Mar 2025 15:46:13 +0000 Subject: [PATCH 1/8] add aurora support --- README.md | 263 +++++++++--------- examples/source/aws-backups.tf | 11 +- modules/aws-backup-source/README.md | 35 +-- modules/aws-backup-source/backup_framework.tf | 40 +++ modules/aws-backup-source/backup_plan.tf | 52 +++- .../aws-backup-source/backup_report_plan.tf | 8 +- .../backup_restore_testing.tf | 16 ++ modules/aws-backup-source/iam.tf | 5 +- modules/aws-backup-source/locals.tf | 1 + modules/aws-backup-source/outputs.tf | 14 + modules/aws-backup-source/variables.tf | 66 +++++ 11 files changed, 354 insertions(+), 157 deletions(-) create mode 100644 modules/aws-backup-source/outputs.tf diff --git a/README.md b/README.md index 5c92c16..18787c1 100644 --- a/README.md +++ b/README.md @@ -2,88 +2,89 @@ ## Introduction -This module addresses the Engineering Red Line [Cloud-6](https://nhs.sharepoint.com/sites/X26_EngineeringCOE/SitePages/Red-lines.aspx#cloud-infrastructure). It provides both a business-as-usual backup facility and the ability to recover from ransomware attacks with a logically air-gapped backup[^1] in a separate AWS account. +This module addresses the Engineering Red Line [Cloud-6](https://nhs.sharepoint.com/sites/X26_EngineeringCOE/SitePages/Red-lines.aspx#cloud-infrastructure). It provides both a business-as-usual backup facility and the ability to recover from ransomware attacks with a logically air-gapped backup[^1] in a separate AWS account. -It is a simple and easy to consume solution for provisioning AWS Backup with terraform[^2]. The headline features we aim to provide are: +It is a simple and easy to consume solution for provisioning AWS Backup with terraform[^2]. The headline features we aim to provide are: -* Immutable storage of persistent data for disaster recovery, with no possibility of human error or malicious tampering. -* Backup and restore within an AWS account, with the potential to support any AWS service that AWS Backup supports. -* Backup and restore to a separate account, allowing for recovery from a situation where the original account is compromised. -* Customisable backup plans to allow for different retention periods and schedules appropriate to the data, product, and service. -* Notifications to alert on backup failures and successes, and reporting for wider visibility. +- Immutable storage of persistent data for disaster recovery, with no possibility of human error or malicious tampering. +- Backup and restore within an AWS account, with the potential to support any AWS service that AWS Backup supports. +- Backup and restore to a separate account, allowing for recovery from a situation where the original account is compromised. +- Customisable backup plans to allow for different retention periods and schedules appropriate to the data, product, and service. +- Notifications to alert on backup failures and successes, and reporting for wider visibility. -This solution does not intend to provide backup and restoration of application code or executable assets. It is *only* for persistent storage of application data that you cannot afford to lose, and typically this will be data that you cannot recreate from another source. +This solution does not intend to provide backup and restoration of application code or executable assets. It is _only_ for persistent storage of application data that you cannot afford to lose, and typically this will be data that you cannot recreate from another source. -Similarly there is no mechanism within this solution to ensure that any schema versions or data formats are compatible with the live version of the application when it is restored. You may wish to include an application version tag in your backups to ensure that you can identify a viable version of the application to restore the data to. +Similarly there is no mechanism within this solution to ensure that any schema versions or data formats are compatible with the live version of the application when it is restored. You may wish to include an application version tag in your backups to ensure that you can identify a viable version of the application to restore the data to. -Setting retention periods and backup schedules will need the input of your Information Asset Owner. We can't choose a default that will apply to all situations. We must not hold data for longer than we have legal rights to do so, and we must also minimise the storage cost; however we must also ensure that we can restore data to a point in time that is useful to the business. Further, to comply fully with [Cloud-6](https://nhs.sharepoint.com/sites/X26_EngineeringCOE/SitePages/Red-lines.aspx#cloud-infrastructure) you will need to test the restoration process. Ransomware often targets backups and seeks not to be discovered. The immutability of the backups provided by this blueprint is a strong defence against this, but to avoid losing all your uncompromised backups, you will need your restoration testing cycle to be *shorter* than the retention period. That will guarantee that any ransomware compromise is discovered before all uncompromised backups are deleted. If your retention period was six months, for instance, you might want to test restoration every two months to ensure that even if one restoration test is missed, there is still an opportunity to catch ransomware before your last good backup expires. +Setting retention periods and backup schedules will need the input of your Information Asset Owner. We can't choose a default that will apply to all situations. We must not hold data for longer than we have legal rights to do so, and we must also minimise the storage cost; however we must also ensure that we can restore data to a point in time that is useful to the business. Further, to comply fully with [Cloud-6](https://nhs.sharepoint.com/sites/X26_EngineeringCOE/SitePages/Red-lines.aspx#cloud-infrastructure) you will need to test the restoration process. Ransomware often targets backups and seeks not to be discovered. The immutability of the backups provided by this blueprint is a strong defence against this, but to avoid losing all your uncompromised backups, you will need your restoration testing cycle to be _shorter_ than the retention period. That will guarantee that any ransomware compromise is discovered before all uncompromised backups are deleted. If your retention period was six months, for instance, you might want to test restoration every two months to ensure that even if one restoration test is missed, there is still an opportunity to catch ransomware before your last good backup expires. Today, the AWS services supported by these modules are: -* S3 -* DynamoDB +- S3 +- DynamoDB +- Aurora -The terraform structure allows any service supported by AWS Backup to be added in the future. If you find that you need to apply this module to a new service, you will find that you can do so but we then need you to contribute those changes back to this repository. +The terraform structure allows any service supported by AWS Backup to be added in the future. If you find that you need to apply this module to a new service, you will find that you can do so but we then need you to contribute those changes back to this repository. -There is some terminology that is important to understand when working with AWS Backup vaults. It uses the terms "governance mode" and "compliance mode". In governance mode, a backup vault can be deleted, and mistakes can be fixed. In compliance mode the backup vault is locked and cannot be deleted. Mistakes persist. The default mode is governance mode. +There is some terminology that is important to understand when working with AWS Backup vaults. It uses the terms "governance mode" and "compliance mode". In governance mode, a backup vault can be deleted, and mistakes can be fixed. In compliance mode the backup vault is locked and cannot be deleted. Mistakes persist. The default mode is governance mode. -*DO NOT SWITCH TO COMPLIANCE MODE UNTIL YOU ARE CERTAIN THAT YOU WANT TO LOCK THE VAULT.* +_DO NOT SWITCH TO COMPLIANCE MODE UNTIL YOU ARE CERTAIN THAT YOU WANT TO LOCK THE VAULT._ -A compliance-mode vault that has passed a cooling-off period is intentionally impossible to delete. This is a feature, not a bug: we want to ensure that the stored data cannot be tampered with by an attacker or their malware, even in the face of collusion. This is good for data that you cannot afford to lose, but it is bad if you have misconfigured retention periods. +A compliance-mode vault that has passed a cooling-off period is intentionally impossible to delete. This is a feature, not a bug: we want to ensure that the stored data cannot be tampered with by an attacker or their malware, even in the face of collusion. This is good for data that you cannot afford to lose, but it is bad if you have misconfigured retention periods. -Again: *DO NOT SWITCH TO COMPLIANCE MODE UNTIL YOU ARE CERTAIN THAT YOU WANT TO LOCK THE VAULT.* +Again: _DO NOT SWITCH TO COMPLIANCE MODE UNTIL YOU ARE CERTAIN THAT YOU WANT TO LOCK THE VAULT._ Please consult the AWS documentation on [vault locks](https://docs.aws.amazon.com/aws-backup/latest/devguide/vault-lock.html) for more information. ### Infrastructure -The code provided here is divided into two modules: `modules/aws-backup-source`, and `modules/aws-backup-destination`. The `source` module is to deploy to any account holding data that needs to be backed up and restored. The `destination` module is to configure a dedicated AWS account to maintain a replicated copy of vault recovery points from the source account: it holds a backup of the backup. You will need both of these accounts provisioned ahead of time to use the full solution, but you can test the source and destination modules within the same account to check that the resources are provisioned correctly. +The code provided here is divided into two modules: `modules/aws-backup-source`, and `modules/aws-backup-destination`. The `source` module is to deploy to any account holding data that needs to be backed up and restored. The `destination` module is to configure a dedicated AWS account to maintain a replicated copy of vault recovery points from the source account: it holds a backup of the backup. You will need both of these accounts provisioned ahead of time to use the full solution, but you can test the source and destination modules within the same account to check that the resources are provisioned correctly. These modules will deploy a number of AWS resources: -* In the source account: - * Vault - * Backup plans - * Restore testing - * Vault policies - * Backup KMS key - * SNS topic for notifications - * Backup framework for compliance -* modules/destination - * Vault - * Vault policies - * Vault lock +- In the source account: + - Vault + - Backup plans + - Restore testing + - Vault policies + - Backup KMS key + - SNS topic for notifications + - Backup framework for compliance +- modules/destination + - Vault + - Vault policies + - Vault lock ![AWS Architecture](./docs/diagrams/aws-architecture.png) -Note that there are always two vaults. In most restoration cases you should only need the `source` vault, so there is no need to copy data from the second, `destination`, vault. The latter is only used in the case of a disaster recovery scenario where the source account is compromised beyond use. As such the recovery time and recovery point objectives you will care about for situations in which the second vault is used should take this into account. +Note that there are always two vaults. In most restoration cases you should only need the `source` vault, so there is no need to copy data from the second, `destination`, vault. The latter is only used in the case of a disaster recovery scenario where the source account is compromised beyond use. As such the recovery time and recovery point objectives you will care about for situations in which the second vault is used should take this into account. ## Developer Guide -This document will guide you through the set-up and deployment of the AWS Backup solution in a typical project. You will need: +This document will guide you through the set-up and deployment of the AWS Backup solution in a typical project. You will need: -* Two AWS accounts: one for the source and one for the destination. The source account will hold the data to be backed up, and the destination account will hold the backup of the backup. You will need to know the account IDs of both accounts, and you will need to be able to create roles and assign permissions within both accounts. -* The ARNs of IAM roles in each account that allows terraform to create the resources. You will need to be able to assume these roles from your CI/CD pipeline. -* A CI/CD pipeline. I'm using GitHub Actions in the examples below, but you can use any CI/CD pipeline that can run terraform. +- Two AWS accounts: one for the source and one for the destination. The source account will hold the data to be backed up, and the destination account will hold the backup of the backup. You will need to know the account IDs of both accounts, and you will need to be able to create roles and assign permissions within both accounts. +- The ARNs of IAM roles in each account that allows terraform to create the resources. You will need to be able to assume these roles from your CI/CD pipeline. +- A CI/CD pipeline. I'm using GitHub Actions in the examples below, but you can use any CI/CD pipeline that can run terraform. -Since it can be tricky to configure terraform to assume different roles within the same invocation, I've split the example configuration into two, with terraform to be applied once per account. The `examples/source` configuration is for the account that holds the data to be backed up, and `examples/destination` is for the account that will hold the backup of the backup. You will need to deploy both configurations to have a complete solution, and output from the `destination` configuration needs to be passed to the `source` configuration, so that the `source` configuration can find the `destination` vault. +Since it can be tricky to configure terraform to assume different roles within the same invocation, I've split the example configuration into two, with terraform to be applied once per account. The `examples/source` configuration is for the account that holds the data to be backed up, and `examples/destination` is for the account that will hold the backup of the backup. You will need to deploy both configurations to have a complete solution, and output from the `destination` configuration needs to be passed to the `source` configuration, so that the `source` configuration can find the `destination` vault. -The other directories in `examples/` hold examples of the permission configuration for the IAM roles in each account. They need to be assigned outside the pipeline itself. For the permissions needed in the destination account, see `examples/destination-bootstrap/permissions.tf`: you may wish to use that file as a starting point for your own permissions. Similarly for the source account, see `examples/source-bootstrap/permissions.tf`. +The other directories in `examples/` hold examples of the permission configuration for the IAM roles in each account. They need to be assigned outside the pipeline itself. For the permissions needed in the destination account, see `examples/destination-bootstrap/permissions.tf`: you may wish to use that file as a starting point for your own permissions. Similarly for the source account, see `examples/source-bootstrap/permissions.tf`. -This blueprint supports backups of S3 and DynamoDB resources, but for brevity this first implementation will only show the backing up of S3 buckets, with DynamoDB backups explicitly disabled. +This blueprint supports backups of S3, DynamoDB and Aurora resources, but for brevity this first implementation will only show the backing up of S3 buckets, with DynamoDB and Aurora backups explicitly disabled. -I will assume that your project uses the [repository template structure](https://github.com/nhs-england-tools/repository-template). In that structure, the terraform configuration is in the `infrastructure/modules` and `infrastructure/environments` directories. The `modules` directory contains the reusable modules, and the `environments` directory contains the environment-specific configuration. If this does not match your project structure, you will need to adapt the instructions accordingly. I will also assume that you are applying this configuration to your `dev` environment, which would be found in the `infrastructure/environments/dev` directory, with `infrastructure/environments/dev/main.tf` as an entry-point. +I will assume that your project uses the [repository template structure](https://github.com/nhs-england-tools/repository-template). In that structure, the terraform configuration is in the `infrastructure/modules` and `infrastructure/environments` directories. The `modules` directory contains the reusable modules, and the `environments` directory contains the environment-specific configuration. If this does not match your project structure, you will need to adapt the instructions accordingly. I will also assume that you are applying this configuration to your `dev` environment, which would be found in the `infrastructure/environments/dev` directory, with `infrastructure/environments/dev/main.tf` as an entry-point. -Similarly I will assume that you have your backup destination account configuration at `infrastructure/environments/dev-backup`. We'll configure that first. +Similarly I will assume that you have your backup destination account configuration at `infrastructure/environments/dev-backup`. We'll configure that first. Copy the `modules/aws-backup-source` and `modules/aws-backup-destination` directories into your `infrastructure/modules` directory, giving you `infrastructure/modules/aws-backup-source` and `infrastructure/modules/aws-backup-destination`. ### IAM roles -Ensure that you have added your IAM role ARNs to your CI/CD pipeline configuration. To follow along with the CI/CD examples below, you should set: +Ensure that you have added your IAM role ARNs to your CI/CD pipeline configuration. To follow along with the CI/CD examples below, you should set: -* `AWS_ROLE_ARN` to the ARN of the role in the source account that terraform will assume to create the resources. -* `AWS_BACKUP_ROLE_ARN` to the ARN of the role in the destination account that terraform will assume to create the resources. +- `AWS_ROLE_ARN` to the ARN of the role in the source account that terraform will assume to create the resources. +- `AWS_BACKUP_ROLE_ARN` to the ARN of the role in the destination account that terraform will assume to create the resources. These values contain the AWS account ID, so where your CI/CD pipeline choice supports them, these values should be treated as secrets. @@ -91,42 +92,42 @@ Add the permissions from the `examples/destination-bootstrap` and `examples/sour ### Destination account configuration -Now, copy the file `examples/destination/aws-backups.tf` to your project at `infrastructure/environments/dev-backup/aws-backups.tf`. Read it and make sure you understand the comments. It instantiates the destination vault, but in order to do so it also needs to create a KMS key to encrypt it. It also needs to know the ARN of terraform's role in the *source* account, to be passed in as the `source_terraform_role_arn` variable. I prefer to configure this as an environment variable in my GitHub Actions pipeline, where the terraform invocation looks like this: +Now, copy the file `examples/destination/aws-backups.tf` to your project at `infrastructure/environments/dev-backup/aws-backups.tf`. Read it and make sure you understand the comments. It instantiates the destination vault, but in order to do so it also needs to create a KMS key to encrypt it. It also needs to know the ARN of terraform's role in the _source_ account, to be passed in as the `source_terraform_role_arn` variable. I prefer to configure this as an environment variable in my GitHub Actions pipeline, where the terraform invocation looks like this: ```yaml - - name: Authenticate with AWS over OIDC for backup - uses: aws-actions/configure-aws-credentials@v4 - with: - role-to-assume: ${{ secrets.ASSUME_BACKUP_ROLE_ARN }} - role-session-name: my-shiny-project-github-deploy-backup-infrastructure - aws-region: eu-west-2 - unset-current-credentials: true - audience: sts.amazonaws.com - - - name: Terraform plan - id: backup_plan - run: | - cd ./terraform/environments/backup - terraform init -backend-config=backend-config/prod.conf - terraform workspace select -or-create prod - terraform init - terraform plan - env: - TF_VAR_source_terraform_role_arn: ${{ secrets.ASSUME_ROLE_ARN }} - - - name: "Terraform apply to backup account" - id: apply_backup - run: | - cd ./terraform/environments/dev-backup - terraform apply -auto-approve -input=false - echo TF_VAR_destination_vault_arn="$(terraform output -raw destination_vault_arn)" >> $GITHUB_ENV - env: - TF_VAR_source_terraform_role_arn: ${{ secrets.ASSUME_ROLE_ARN }} +- name: Authenticate with AWS over OIDC for backup + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ secrets.ASSUME_BACKUP_ROLE_ARN }} + role-session-name: my-shiny-project-github-deploy-backup-infrastructure + aws-region: eu-west-2 + unset-current-credentials: true + audience: sts.amazonaws.com + +- name: Terraform plan + id: backup_plan + run: | + cd ./terraform/environments/backup + terraform init -backend-config=backend-config/prod.conf + terraform workspace select -or-create prod + terraform init + terraform plan + env: + TF_VAR_source_terraform_role_arn: ${{ secrets.ASSUME_ROLE_ARN }} + +- name: "Terraform apply to backup account" + id: apply_backup + run: | + cd ./terraform/environments/dev-backup + terraform apply -auto-approve -input=false + echo TF_VAR_destination_vault_arn="$(terraform output -raw destination_vault_arn)" >> $GITHUB_ENV + env: + TF_VAR_source_terraform_role_arn: ${{ secrets.ASSUME_ROLE_ARN }} ``` -Note that in the last step the `ASSUME_ROLE_ARN` is the ARN of the role in the *source* account, not the role in the destination account. You will also see here that we're passing the `destination_vault_arn` output from the destination configuration to the environment, so that the source configuration can find the destination vault. +Note that in the last step the `ASSUME_ROLE_ARN` is the ARN of the role in the _source_ account, not the role in the destination account. You will also see here that we're passing the `destination_vault_arn` output from the destination configuration to the environment, so that the source configuration can find the destination vault. -This means that the destination configuration needs to be applied *before* the source configuration. If you are using the above configuration, insert it before the OIDC authentication step for the source configuration. +This means that the destination configuration needs to be applied _before_ the source configuration. If you are using the above configuration, insert it before the OIDC authentication step for the source configuration. In case the terraform state S3 bucket and DynamoDB lock table information are required then please add the following in the file `infrastructure/environments/dev-backup/aws-backups.tf` as appropriate: @@ -141,7 +142,8 @@ terraform { } } ``` -To choose which resources will be backed up by AWS Backup, you need to tag them with the tag `NHSE-Enable-Backup`. So do that now: in your *existing* terraform configuration, add the tag to the resources that you want to back up, and apply it. For example, if you want to back up an S3 bucket, you would add the tag to the bucket resource: + +To choose which resources will be backed up by AWS Backup, you need to tag them with the tag `NHSE-Enable-Backup`. So do that now: in your _existing_ terraform configuration, add the tag to the resources that you want to back up, and apply it. For example, if you want to back up an S3 bucket, you would add the tag to the bucket resource: ```terraform resource "aws_s3_bucket" "my_precious_bucket" { @@ -153,9 +155,9 @@ resource "aws_s3_bucket" "my_precious_bucket" { } ``` -The backup plan supplied in the module configuration will back up all resources with this tag, and the matching is case-sensitive. It *will not* match a resource tagged `true` or `TRUE`, only `True`. +The backup plan supplied in the module configuration will back up all resources with this tag, and the matching is case-sensitive. It _will not_ match a resource tagged `true` or `TRUE`, only `True`. -One detail to be aware of is that AWS insists on versioning being switched on to back up S3 buckets. If you don't already have a lifecycle policy set for the bucket you want to protect, this will work: +One detail to be aware of is that AWS insists on versioning being switched on to back up S3 buckets. If you don't already have a lifecycle policy set for the bucket you want to protect, this will work: ```terraform resource "aws_s3_bucket_versioning" "my_precious_bucket" { @@ -182,9 +184,9 @@ resource "aws_s3_bucket_lifecycle_configuration" "my_precious_bucket" { ### Source account configuration -Now copy the file `examples/source/aws-backups.tf` to your project as `infrastructure/environments/dev/aws-backups.tf`. Read it and make sure you understand the comments. +Now copy the file `examples/source/aws-backups.tf` to your project as `infrastructure/environments/dev/aws-backups.tf`. Read it and make sure you understand the comments. -In addition to the core backup vault and associated resources supplied by the `aws-backup-source` module, the example configuration supplies an S3 bucket for backup reports to be stored in, and a KMS key for notifications to be encrypted with. These are likely to be moved into the `aws-backup-source` module in future versions, but for now they are in the example configuration because the project from which the blueprint was extracted from had these resources already in place. +In addition to the core backup vault and associated resources supplied by the `aws-backup-source` module, the example configuration supplies an S3 bucket for backup reports to be stored in, and a KMS key for notifications to be encrypted with. These are likely to be moved into the `aws-backup-source` module in future versions, but for now they are in the example configuration because the project from which the blueprint was extracted from had these resources already in place. The backup plan configuration in `examples/source/aws-backups.tf` looks like this: @@ -209,76 +211,75 @@ The backup plan configuration in `examples/source/aws-backups.tf` looks like thi } ``` -You must change the numbers here to match the requirements of your project. The `copy_action` `delete_after` entry is the number of days that the backup will be kept in the destination account, so in this example we will be retaining snapshots in the air-gapped account for four days. The `lifecycle` `delete_after` entry is the number of days that the backup will be kept in the source account. The value of two days in this example is shorter, but you may wish to either match the destination and source (so that you know any individual snapshot is in more than one place), or make the duration in the source account longer than in the destination to minimise the storage cost while retaining a level of contingency. +You must change the numbers here to match the requirements of your project. The `copy_action` `delete_after` entry is the number of days that the backup will be kept in the destination account, so in this example we will be retaining snapshots in the air-gapped account for four days. The `lifecycle` `delete_after` entry is the number of days that the backup will be kept in the source account. The value of two days in this example is shorter, but you may wish to either match the destination and source (so that you know any individual snapshot is in more than one place), or make the duration in the source account longer than in the destination to minimise the storage cost while retaining a level of contingency. -As mentioned in the introduction, you will need to speak to your Information Asset Owner to determine the correct values for your project. The values here are intentionally too short for a practical production system, and will fail checks built into the backup framework. +As mentioned in the introduction, you will need to speak to your Information Asset Owner to determine the correct values for your project. The values here are intentionally too short for a practical production system, and will fail checks built into the backup framework. -The `name` is an arbitrary label. Change it to match the actual schedule. It will appear in the AWS Backup console, so make it meaningful. +The `name` is an arbitrary label. Change it to match the actual schedule. It will appear in the AWS Backup console, so make it meaningful. -The `schedule` is a cron expression that determines when the backup will be taken. If you are unfamiliar with cron syntax, AWS document it [here](https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-scheduled-rule-pattern.html#eb-cron-expressions). In this example, the backup will be taken at midnight every day. If you are testing this configuration in a development environment, you may wish to change this to a more frequent schedule, or to a more convenient time of day. +The `schedule` is a cron expression that determines when the backup will be taken. If you are unfamiliar with cron syntax, AWS document it [here](https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-scheduled-rule-pattern.html#eb-cron-expressions). In this example, the backup will be taken at midnight every day. If you are testing this configuration in a development environment, you may wish to change this to a more frequent schedule, or to a more convenient time of day. -The final detail to be aware of here is the `selection_tag`. This defines the tag that AWS Backup will use to determine which resources to back up, and which you will have seen above. Resources that you want to back up must have this tag. If you have followed the instructions above, you will have already tagged the resources you want to back up. The name of the tag has been chosen such that it is unlikely to conflict with any existing tags. +The final detail to be aware of here is the `selection_tag`. This defines the tag that AWS Backup will use to determine which resources to back up, and which you will have seen above. Resources that you want to back up must have this tag. If you have followed the instructions above, you will have already tagged the resources you want to back up. The name of the tag has been chosen such that it is unlikely to conflict with any existing tags. -The `aws-backup-source` module requires the ARN of the role in the destination account that terraform will assume to create the resources. This is passed in as the `destination_terraform_role_arn` variable. In my usage of the blueprint, this is passed in as an environment variable in the GitHub Actions pipeline. The terraform invocation looks like this: +The `aws-backup-source` module requires the ARN of the role in the destination account that terraform will assume to create the resources. This is passed in as the `destination_terraform_role_arn` variable. In my usage of the blueprint, this is passed in as an environment variable in the GitHub Actions pipeline. The terraform invocation looks like this: ```yaml - - name: Authenticate with AWS over OIDC - uses: aws-actions/configure-aws-credentials@v4 - with: - role-to-assume: ${{ secrets.ASSUME_ROLE_ARN }} - role-session-name: tech-radar-backend-github-deploy-infrastructure - aws-region: eu-west-2 - unset-current-credentials: true - audience: sts.amazonaws.com - - - name: Terraform plan - id: plan - run: | - cd ./terraform/environments/main - terraform init -backend-config=backend-config/prod.conf - terraform workspace select -or-create prod - terraform init - terraform plan - - - name: "Terraform apply to main account" - run: | - cd ./terraform/environments/main - terraform apply -auto-approve -input=false +- name: Authenticate with AWS over OIDC + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ secrets.ASSUME_ROLE_ARN }} + role-session-name: tech-radar-backend-github-deploy-infrastructure + aws-region: eu-west-2 + unset-current-credentials: true + audience: sts.amazonaws.com + +- name: Terraform plan + id: plan + run: | + cd ./terraform/environments/main + terraform init -backend-config=backend-config/prod.conf + terraform workspace select -or-create prod + terraform init + terraform plan + +- name: "Terraform apply to main account" + run: | + cd ./terraform/environments/main + terraform apply -auto-approve -input=false ``` -When the earlier step wrote to the `$GITHUB_ENV` file, it wrote the `destination_vault_arn` output from the destination configuration. This is passed in as the `destination_vault_arn` variable to the source configuration, so we don't need to explicitly configure it here. Your CI/CD pipeline will need to ensure that the destination configuration is applied before the source configuration and that the `destination_vault_arn` output is passed along, and probably has a similar mechanism to the above. +When the earlier step wrote to the `$GITHUB_ENV` file, it wrote the `destination_vault_arn` output from the destination configuration. This is passed in as the `destination_vault_arn` variable to the source configuration, so we don't need to explicitly configure it here. Your CI/CD pipeline will need to ensure that the destination configuration is applied before the source configuration and that the `destination_vault_arn` output is passed along, and probably has a similar mechanism to the above. ## Next steps -* If you haven't already, set up the appropriate steps in your CI/CD pipeline to deploy the destination and source configurations. -* Run the deployment. You want to only do this from your CI/CD pipeline, so that the roles are assumed correctly, and so that you know it is properly automated. -* Check the AWS Backup console to ensure that the vaults have been created, and that the backup plans are in place. -* Allow a backup to run, and check that the backup checkpoint appears in both vaults. -* Test a restoration from the source vault. This is a critical step, as you need to know that you can restore from the source vault in the event of a ransomware attack. If you can't, you need to know now, not when you're under attack. -* Test a restoration from the destination vault. -* Document your restoration processes so that you can repeat the test in the future. -* Configure and test notifications. See the `notifications_target_email_address` variable in the `aws-backup-source` module for an example of how to configure this. -* Check that the backup reports are being stored in the S3 bucket. -* Switch to compliance mode in the destination vault. You may have caught the hints above that this is a one-way operation, so make sure you're ready to do it. But do it: it's the end point we need to reach. You will need to set the `enable_vault_protection`, `vault_lock_type`, and `changeable_for_days` variables correctly in the `destination` module. +- If you haven't already, set up the appropriate steps in your CI/CD pipeline to deploy the destination and source configurations. +- Run the deployment. You want to only do this from your CI/CD pipeline, so that the roles are assumed correctly, and so that you know it is properly automated. +- Check the AWS Backup console to ensure that the vaults have been created, and that the backup plans are in place. +- Allow a backup to run, and check that the backup checkpoint appears in both vaults. +- Test a restoration from the source vault. This is a critical step, as you need to know that you can restore from the source vault in the event of a ransomware attack. If you can't, you need to know now, not when you're under attack. +- Test a restoration from the destination vault. +- Document your restoration processes so that you can repeat the test in the future. +- Configure and test notifications. See the `notifications_target_email_address` variable in the `aws-backup-source` module for an example of how to configure this. +- Check that the backup reports are being stored in the S3 bucket. +- Switch to compliance mode in the destination vault. You may have caught the hints above that this is a one-way operation, so make sure you're ready to do it. But do it: it's the end point we need to reach. You will need to set the `enable_vault_protection`, `vault_lock_type`, and `changeable_for_days` variables correctly in the `destination` module. ## FAQs -None yet. If you have a question, please raise an issue. +None yet. If you have a question, please raise an issue. ## Procedural notes -This is very much a v1 product, so thank you for persisting. Obvious features that we will want to add, and that you can expect in the future, include: - -* Centralised reporting on installation and backup status. -* Moving the S3 bucket and KMS key from the source configuration to the source module, and the KMS key from the destination configuration to the destination module. This will shrink the footprint of the example configuration and make it easier to use. -* Support for more AWS services. This is on you: if you need a service backed up that isn't already supported, we need you to add it to the module and contribute it back. -* Better configuration defaults, so unused services can be omitted and retention periods can be set more easily. -* Provide a docker container to execute the terraform code so that non-terraform users can use it. -* Switch to logically air-gapped vaults when the feature is more mature (specifically, when RDS is fully supported). -* Built-in restoration tooling. +This is very much a v1 product, so thank you for persisting. Obvious features that we will want to add, and that you can expect in the future, include: -Keep an eye on the [issues](https://github.com/NHSDigital/terraform-aws-backup/issues) for updates on these and anything else that catches your eye. If you have a feature request, please raise an issue. If you have a bug report, please raise an issue. If you have a fix, please raise a pull request. If you want to ask "why on earth did you do it *that* way", raise an issue, and we'll improve this README. +- Centralised reporting on installation and backup status. +- Moving the S3 bucket and KMS key from the source configuration to the source module, and the KMS key from the destination configuration to the destination module. This will shrink the footprint of the example configuration and make it easier to use. +- Support for more AWS services. This is on you: if you need a service backed up that isn't already supported, we need you to add it to the module and contribute it back. +- Better configuration defaults, so unused services can be omitted and retention periods can be set more easily. +- Provide a docker container to execute the terraform code so that non-terraform users can use it. +- Switch to logically air-gapped vaults when the feature is more mature (specifically, when RDS is fully supported). +- Built-in restoration tooling. -[^1]: While this blueprint was being written, AWS released [logically air-gapped vaults](https://docs.aws.amazon.com/aws-backup/latest/devguide/logicallyairgappedvault.html) to AWS Backup. This solution does not use that feature because at time of writing it lacks support for some critical use cases. It is reasonable to assume that at some point a future release of this blueprint will switch to it, as it will allow a reduction of complexity and cost. +Keep an eye on the [issues](https://github.com/NHSDigital/terraform-aws-backup/issues) for updates on these and anything else that catches your eye. If you have a feature request, please raise an issue. If you have a bug report, please raise an issue. If you have a fix, please raise a pull request. If you want to ask "why on earth did you do it _that_ way", raise an issue, and we'll improve this README. -[^2]: You may decide that this statement is a cruel lie. Please raise an issue or, better still, a pull request. We want to make this as simple as possible to use, and what's obvious to me might not be obvious to you. We can only make it better with your feedback. +[^1]: While this blueprint was being written, AWS released [logically air-gapped vaults](https://docs.aws.amazon.com/aws-backup/latest/devguide/logicallyairgappedvault.html) to AWS Backup. This solution does not use that feature because at time of writing it lacks support for some critical use cases. It is reasonable to assume that at some point a future release of this blueprint will switch to it, as it will allow a reduction of complexity and cost. +[^2]: You may decide that this statement is a cruel lie. Please raise an issue or, better still, a pull request. We want to make this as simple as possible to use, and what's obvious to me might not be obvious to you. We can only make it better with your feedback. diff --git a/examples/source/aws-backups.tf b/examples/source/aws-backups.tf index 0186d66..7747620 100644 --- a/examples/source/aws-backups.tf +++ b/examples/source/aws-backups.tf @@ -112,7 +112,7 @@ module "source" { ], "selection_tag": "NHSE-Enable-Backup" } - # Note here that we need to explicitly disable DynamoDB backups in the source account. + # Note here that we need to explicitly disable DynamoDB and Aurora backups in the source account. # The default config in the module enables backups for all resource types. backup_plan_config_dynamodb = { "compliance_resource_types": [ @@ -123,4 +123,13 @@ module "source" { "enable": false, "selection_tag": "NHSE-Enable-Backup" } + backup_plan_config_aurora = { + "compliance_resource_types": [ + "Aurora" + ], + "rules": [ + ], + "enable": false, + "selection_tag": "NHSE-Enable-Backup" + } } diff --git a/modules/aws-backup-source/README.md b/modules/aws-backup-source/README.md index 6b9a409..c425572 100644 --- a/modules/aws-backup-source/README.md +++ b/modules/aws-backup-source/README.md @@ -4,23 +4,24 @@ The AWS Backup Module helps automates the setup of AWS Backup resources in a sou ## Inputs -| Name | Description | Type | Default | Required | -|------|-------------|------|---------|:--------:| -| [backup\_copy\_vault\_account\_id](#input\_backup\_copy\_vault\_account\_id) | The account id of the destination backup vault for allowing restores back into the source account. | `string` | `""` | no | -| [backup\_copy\_vault\_arn](#input\_backup\_copy\_vault\_arn) | The ARN of the destination backup vault for cross-account backup copies. | `string` | `""` | no | -| [backup\_plan\_config](#input\_backup\_plan\_config) | Configuration for backup plans |
object({
selection_tag = string
compliance_resource_types = list(string)
rules = list(object({
name = string
schedule = string
enable_continuous_backup = optional(bool)
lifecycle = object({
delete_after = optional(number)
cold_storage_after = optional(number)
})
copy_action = optional(object({
delete_after = optional(number)
}))
}))
})
|
{
"compliance_resource_types": [
"S3"
],
"rules": [
{
"copy_action": {
"delete_after": 365
},
"lifecycle": {
"delete_after": 35
},
"name": "daily_kept_5_weeks",
"schedule": "cron(0 0 * * ? *)"
},
{
"copy_action": {
"delete_after": 365
},
"lifecycle": {
"delete_after": 90
},
"name": "weekly_kept_3_months",
"schedule": "cron(0 1 ? * SUN *)"
},
{
"copy_action": {
"delete_after": 365
},
"lifecycle": {
"cold_storage_after": 30,
"delete_after": 2555
},
"name": "monthly_kept_7_years",
"schedule": "cron(0 2 1 * ? *)"
},
{
"copy_action": {
"delete_after": 365
},
"enable_continuous_backup": true,
"lifecycle": {
"delete_after": 35
},
"name": "point_in_time_recovery",
"schedule": "cron(0 5 * * ? *)"
}
],
"selection_tag": "BackupLocal"
}
| no | -| [backup\_plan\_config\_dynamodb](#input\_backup\_plan\_config\_dynamodb) | Configuration for backup plans with dynamodb |
object({
enable = bool
selection_tag = string
compliance_resource_types = list(string)
rules = optional(list(object({
name = string
schedule = string
enable_continuous_backup = optional(bool)
lifecycle = object({
delete_after = number
cold_storage_after = optional(number)
})
copy_action = optional(object({
delete_after = optional(number)
}))
})))
})
|
{
"compliance_resource_types": [
"DynamoDB"
],
"enable": true,
"rules": [
{
"copy_action": {
"delete_after": 365
},
"lifecycle": {
"delete_after": 35
},
"name": "dynamodb_daily_kept_5_weeks",
"schedule": "cron(0 0 * * ? *)"
},
{
"copy_action": {
"delete_after": 365
},
"lifecycle": {
"delete_after": 90
},
"name": "dynamodb_weekly_kept_3_months",
"schedule": "cron(0 1 ? * SUN *)"
},
{
"copy_action": {
"delete_after": 365
},
"lifecycle": {
"cold_storage_after": 30,
"delete_after": 2555
},
"name": "dynamodb_monthly_kept_7_years",
"schedule": "cron(0 2 1 * ? *)"
}
],
"selection_tag": "BackupDynamoDB"
}
| no | -| [bootstrap\_kms\_key\_arn](#input\_bootstrap\_kms\_key\_arn) | The ARN of the bootstrap KMS key used for encryption at rest of the SNS topic. | `string` | n/a | yes | -| [environment\_name](#input\_environment\_name) | The name of the environment where AWS Backup is configured. | `string` | n/a | yes | -| [notifications\_target\_email\_address](#input\_notifications\_target\_email\_address) | The email address to which backup notifications will be sent via SNS. | `string` | `""` | no | -| [project\_name](#input\_project\_name) | The name of the project this relates to. | `string` | n/a | yes | -| [reports\_bucket](#input\_reports\_bucket) | Bucket to drop backup reports into | `string` | n/a | yes | -| [restore\_testing\_plan\_algorithm](#input\_restore\_testing\_plan\_algorithm) | Algorithm of the Recovery Selection Point | `string` | `"LATEST_WITHIN_WINDOW"` | no | -| [restore\_testing\_plan\_recovery\_point\_types](#input\_restore\_testing\_plan\_recovery\_point\_types) | Recovery Point Types | `list(string)` |
[
"SNAPSHOT"
]
| no | -| [restore\_testing\_plan\_scheduled\_expression](#input\_restore\_testing\_plan\_scheduled\_expression) | Scheduled Expression of Recovery Selection Point | `string` | `"cron(0 1 ? * SUN *)"` | no | -| [restore\_testing\_plan\_selection\_window\_days](#input\_restore\_testing\_plan\_selection\_window\_days) | Selection window days | `number` | `7` | no | -| [restore\_testing\_plan\_start\_window](#input\_restore\_testing\_plan\_start\_window) | Start window from the scheduled time during which the test should start | `number` | `1` | no | -| [terraform\_role\_arn](#input\_terraform\_role\_arn) | ARN of Terraform role used to deploy to account | `string` | n/a | yes | +| Name | Description | Type | Default | Required | +| --------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------: | +| [backup_copy_vault_account_id](#input_backup_copy_vault_account_id) | The account id of the destination backup vault for allowing restores back into the source account. | `string` | `""` | no | +| [backup_copy_vault_arn](#input_backup_copy_vault_arn) | The ARN of the destination backup vault for cross-account backup copies. | `string` | `""` | no | +| [backup_plan_config](#input_backup_plan_config) | Configuration for backup plans |
object({
selection_tag = string
compliance_resource_types = list(string)
rules = list(object({
name = string
schedule = string
enable_continuous_backup = optional(bool)
lifecycle = object({
delete_after = optional(number)
cold_storage_after = optional(number)
})
copy_action = optional(object({
delete_after = optional(number)
}))
}))
})
|
{
"compliance_resource_types": [
"S3"
],
"rules": [
{
"copy_action": {
"delete_after": 365
},
"lifecycle": {
"delete_after": 35
},
"name": "daily_kept_5_weeks",
"schedule": "cron(0 0 * * ? *)"
},
{
"copy_action": {
"delete_after": 365
},
"lifecycle": {
"delete_after": 90
},
"name": "weekly_kept_3_months",
"schedule": "cron(0 1 ? * SUN *)"
},
{
"copy_action": {
"delete_after": 365
},
"lifecycle": {
"cold_storage_after": 30,
"delete_after": 2555
},
"name": "monthly_kept_7_years",
"schedule": "cron(0 2 1 * ? *)"
},
{
"copy_action": {
"delete_after": 365
},
"enable_continuous_backup": true,
"lifecycle": {
"delete_after": 35
},
"name": "point_in_time_recovery",
"schedule": "cron(0 5 * * ? *)"
}
],
"selection_tag": "BackupLocal"
}
| no | +| [backup_plan_config_dynamodb](#input_backup_plan_config_dynamodb) | Configuration for backup plans with dynamodb |
object({
enable = bool
selection_tag = string
compliance_resource_types = list(string)
rules = optional(list(object({
name = string
schedule = string
enable_continuous_backup = optional(bool)
lifecycle = object({
delete_after = number
cold_storage_after = optional(number)
})
copy_action = optional(object({
delete_after = optional(number)
}))
})))
})
|
{
"compliance_resource_types": [
"DynamoDB"
],
"enable": true,
"rules": [
{
"copy_action": {
"delete_after": 365
},
"lifecycle": {
"delete_after": 35
},
"name": "dynamodb_daily_kept_5_weeks",
"schedule": "cron(0 0 * * ? *)"
},
{
"copy_action": {
"delete_after": 365
},
"lifecycle": {
"delete_after": 90
},
"name": "dynamodb_weekly_kept_3_months",
"schedule": "cron(0 1 ? * SUN *)"
},
{
"copy_action": {
"delete_after": 365
},
"lifecycle": {
"cold_storage_after": 30,
"delete_after": 2555
},
"name": "dynamodb_monthly_kept_7_years",
"schedule": "cron(0 2 1 * ? *)"
}
],
"selection_tag": "BackupDynamoDB"
}
| no | +| [backup_plan_config_aurora](#input_backup_plan_config_aurora) | Configuration for backup plans with aurora |
object({
enable = bool
selection_tag = string
compliance_resource_types = list(string)
restore_testing_overrides = optional(string)
rules = optional(list(object({
name = string
schedule = string
enable_continuous_backup = optional(bool)
lifecycle = object({
delete_after = number
cold_storage_after = optional(number)
})
copy_action = optional(object({
delete_after = optional(number)
}))
})))
})
|
{
"compliance_resource_types": [
"Aurora"
],
"enable": true,
"restore_testing_overrides" : "{\"dbsubnetgroupname\": \"test-subnet\"}",
"rules": [
{
"copy_action": {
"delete_after": 365
},
"lifecycle": {
"delete_after": 35
},
"name": "aurora_daily_kept_5_weeks",
"schedule": "cron(0 0 * * ? *)"
},
{
"copy_action": {
"delete_after": 365
},
"lifecycle": {
"delete_after": 90
},
"name": "aurora_weekly_kept_3_months",
"schedule": "cron(0 1 ? * SUN *)"
},
{
"copy_action": {
"delete_after": 365
},
"lifecycle": {
"cold_storage_after": 30,
"delete_after": 2555
},
"name": "aurora_monthly_kept_7_years",
"schedule": "cron(0 2 1 * ? *)"
}
],
"selection_tag": "BackupAurora"
}
| no | +| [bootstrap_kms_key_arn](#input_bootstrap_kms_key_arn) | The ARN of the bootstrap KMS key used for encryption at rest of the SNS topic. | `string` | n/a | yes | +| [environment_name](#input_environment_name) | The name of the environment where AWS Backup is configured. | `string` | n/a | yes | +| [notifications_target_email_address](#input_notifications_target_email_address) | The email address to which backup notifications will be sent via SNS. | `string` | `""` | no | +| [project_name](#input_project_name) | The name of the project this relates to. | `string` | n/a | yes | +| [reports_bucket](#input_reports_bucket) | Bucket to drop backup reports into | `string` | n/a | yes | +| [restore_testing_plan_algorithm](#input_restore_testing_plan_algorithm) | Algorithm of the Recovery Selection Point | `string` | `"LATEST_WITHIN_WINDOW"` | no | +| [restore_testing_plan_recovery_point_types](#input_restore_testing_plan_recovery_point_types) | Recovery Point Types | `list(string)` |
[
"SNAPSHOT"
]
| no | +| [restore_testing_plan_scheduled_expression](#input_restore_testing_plan_scheduled_expression) | Scheduled Expression of Recovery Selection Point | `string` | `"cron(0 1 ? * SUN *)"` | no | +| [restore_testing_plan_selection_window_days](#input_restore_testing_plan_selection_window_days) | Selection window days | `number` | `7` | no | +| [restore_testing_plan_start_window](#input_restore_testing_plan_start_window) | Start window from the scheduled time during which the test should start | `number` | `1` | no | +| [terraform_role_arn](#input_terraform_role_arn) | ARN of Terraform role used to deploy to account | `string` | n/a | yes | ## Example diff --git a/modules/aws-backup-source/backup_framework.tf b/modules/aws-backup-source/backup_framework.tf index d10b431..1994101 100644 --- a/modules/aws-backup-source/backup_framework.tf +++ b/modules/aws-backup-source/backup_framework.tf @@ -147,3 +147,43 @@ resource "aws_backup_framework" "dynamodb" { } } } + +resource "aws_backup_framework" "aurora" { + count = var.backup_plan_config_aurora.enable ? 1 : 0 + # must be underscores instead of dashes + name = replace("${local.resource_name_prefix}-aurora-framework", "-", "_") + description = "${var.project_name} Aurora Backup Framework" + + # Evaluates if resources are protected by a backup plan. + control { + name = "BACKUP_RESOURCES_PROTECTED_BY_BACKUP_PLAN" + + scope { + compliance_resource_types = var.backup_plan_config_aurora.compliance_resource_types + tags = { + (var.backup_plan_config_aurora.selection_tag) = "True" + } + } + } + # Evaluates if resources have at least one recovery point created within the past 1 day. + control { + name = "BACKUP_LAST_RECOVERY_POINT_CREATED" + + input_parameter { + name = "recoveryPointAgeUnit" + value = "days" + } + + input_parameter { + name = "recoveryPointAgeValue" + value = "1" + } + + scope { + compliance_resource_types = var.backup_plan_config_aurora.compliance_resource_types + tags = { + (var.backup_plan_config_aurora.selection_tag) = "True" + } + } + } +} diff --git a/modules/aws-backup-source/backup_plan.tf b/modules/aws-backup-source/backup_plan.tf index 7d055e1..c5df113 100644 --- a/modules/aws-backup-source/backup_plan.tf +++ b/modules/aws-backup-source/backup_plan.tf @@ -7,10 +7,10 @@ resource "aws_backup_plan" "default" { recovery_point_tags = { backup_rule_name = rule.value.name } - rule_name = rule.value.name - target_vault_name = aws_backup_vault.main.name - schedule = rule.value.schedule - enable_continuous_backup = rule.value.enable_continuous_backup != null ? rule.value.enable_continuous_backup : null + rule_name = rule.value.name + target_vault_name = aws_backup_vault.main.name + schedule = rule.value.schedule + enable_continuous_backup = rule.value.enable_continuous_backup != null ? rule.value.enable_continuous_backup : null lifecycle { delete_after = rule.value.lifecycle.delete_after != null ? rule.value.lifecycle.delete_after : null cold_storage_after = rule.value.lifecycle.cold_storage_after != null ? rule.value.lifecycle.cold_storage_after : null @@ -59,6 +59,37 @@ resource "aws_backup_plan" "dynamodb" { } } +# this backup plan shouldn't include a continous backup rule as it isn't supported for Aurora +resource "aws_backup_plan" "aurora" { + count = var.backup_plan_config_aurora.enable ? 1 : 0 + name = "${local.resource_name_prefix}-aurora-plan" + + dynamic "rule" { + for_each = var.backup_plan_config_aurora.rules + content { + recovery_point_tags = { + backup_rule_name = rule.value.name + } + rule_name = rule.value.name + target_vault_name = aws_backup_vault.main.name + schedule = rule.value.schedule + lifecycle { + delete_after = rule.value.lifecycle.delete_after != null ? rule.value.lifecycle.delete_after : null + cold_storage_after = rule.value.lifecycle.cold_storage_after != null ? rule.value.lifecycle.cold_storage_after : null + } + dynamic "copy_action" { + for_each = var.backup_copy_vault_arn != "" && var.backup_copy_vault_account_id != "" && rule.value.copy_action != null ? rule.value.copy_action : {} + content { + lifecycle { + delete_after = copy_action.value + } + destination_vault_arn = var.backup_copy_vault_arn + } + } + } + } +} + resource "aws_backup_selection" "default" { iam_role_arn = aws_iam_role.backup.arn name = "${local.resource_name_prefix}-selection" @@ -83,3 +114,16 @@ resource "aws_backup_selection" "dynamodb" { value = "True" } } + +resource "aws_backup_selection" "aurora" { + count = var.backup_plan_config_aurora.enable ? 1 : 0 + iam_role_arn = aws_iam_role.backup.arn + name = "${local.resource_name_prefix}-aurora-selection" + plan_id = aws_backup_plan.aurora[0].id + + selection_tag { + key = var.backup_plan_config_aurora.selection_tag + type = "STRINGEQUALS" + value = "True" + } +} diff --git a/modules/aws-backup-source/backup_report_plan.tf b/modules/aws-backup-source/backup_report_plan.tf index e0f20d4..d1359ba 100644 --- a/modules/aws-backup-source/backup_report_plan.tf +++ b/modules/aws-backup-source/backup_report_plan.tf @@ -47,8 +47,12 @@ resource "aws_backup_report_plan" "resource_compliance" { } report_setting { - framework_arns = var.backup_plan_config_dynamodb.enable ? [aws_backup_framework.main.arn, aws_backup_framework.dynamodb[0].arn] : [aws_backup_framework.main.arn] - number_of_frameworks = 2 + framework_arns = compact([ + aws_backup_framework.main.arn, + var.backup_plan_config_dynamodb.enable ? aws_backup_framework.dynamodb[0].arn : "", + var.backup_plan_config_aurora.enable ? aws_backup_framework.aurora[0].arn : "" + ]) + number_of_frameworks = 3 report_template = "RESOURCE_COMPLIANCE_REPORT" } } diff --git a/modules/aws-backup-source/backup_restore_testing.tf b/modules/aws-backup-source/backup_restore_testing.tf index 6c4b6f3..ee81ba1 100644 --- a/modules/aws-backup-source/backup_restore_testing.tf +++ b/modules/aws-backup-source/backup_restore_testing.tf @@ -24,3 +24,19 @@ resource "awscc_backup_restore_testing_selection" "backup_restore_testing_select }] } } + +resource "awscc_backup_restore_testing_selection" "backup_restore_testing_selection_aurora" { + count = var.backup_plan_config_aurora.enable ? 1 : 0 + iam_role_arn = aws_iam_role.backup.arn + protected_resource_type = "Aurora" + restore_testing_plan_name = awscc_backup_restore_testing_plan.backup_restore_testing_plan.restore_testing_plan_name + restore_testing_selection_name = "backup_restore_testing_selection_aurora" + protected_resource_arns = ["*"] + protected_resource_conditions = { + string_equals = [{ + key = "aws:ResourceTag/${var.backup_plan_config_aurora.selection_tag}" + value = "True" + }] + } + restore_metadata_overrides = local.aurora_overrides +} diff --git a/modules/aws-backup-source/iam.tf b/modules/aws-backup-source/iam.tf index e4d58dc..3b81513 100644 --- a/modules/aws-backup-source/iam.tf +++ b/modules/aws-backup-source/iam.tf @@ -12,8 +12,9 @@ data "aws_iam_policy_document" "assume_role" { } resource "aws_iam_role" "backup" { - name = "${var.project_name}BackupRole" - assume_role_policy = data.aws_iam_policy_document.assume_role.json + name = "${var.project_name}BackupRole" + assume_role_policy = data.aws_iam_policy_document.assume_role.json + permissions_boundary = length(var.iam_role_permissions_boundary) > 0 ? var.iam_role_permissions_boundary : null } resource "aws_iam_role_policy_attachment" "backup" { diff --git a/modules/aws-backup-source/locals.tf b/modules/aws-backup-source/locals.tf index e692981..dc7da9c 100644 --- a/modules/aws-backup-source/locals.tf +++ b/modules/aws-backup-source/locals.tf @@ -1,3 +1,4 @@ locals { resource_name_prefix = "${data.aws_region.current.name}-${data.aws_caller_identity.current.account_id}-backup" + aurora_overrides = jsondecode(var.backup_plan_config_aurora.restore_testing_overrides) } diff --git a/modules/aws-backup-source/outputs.tf b/modules/aws-backup-source/outputs.tf new file mode 100644 index 0000000..96ab936 --- /dev/null +++ b/modules/aws-backup-source/outputs.tf @@ -0,0 +1,14 @@ +output "backup_role_arn" { + value = aws_iam_role.backup.arn + description = "ARN of the of the backup role" +} + +output "backup_vault_arn" { + value = aws_backup_vault.main.arn + description = "ARN of the of the vault" +} + +output "backup_vault_name" { + value = aws_backup_vault.main.name + description = "Name of the of the vault" +} diff --git a/modules/aws-backup-source/variables.tf b/modules/aws-backup-source/variables.tf index 0873063..267bc93 100644 --- a/modules/aws-backup-source/variables.tf +++ b/modules/aws-backup-source/variables.tf @@ -197,3 +197,69 @@ variable "backup_plan_config_dynamodb" { ] } } + +variable "backup_plan_config_aurora" { + description = "Configuration for backup plans with aurora" + type = object({ + enable = bool + selection_tag = string + compliance_resource_types = list(string) + restore_testing_overrides = optional(string) + rules = optional(list(object({ + name = string + schedule = string + enable_continuous_backup = optional(bool) + lifecycle = object({ + delete_after = number + cold_storage_after = optional(number) + }) + copy_action = optional(object({ + delete_after = optional(number) + })) + }))) + }) + default = { + enable = true + selection_tag = "BackupAurora" + compliance_resource_types = ["Aurora"] + rules = [ + { + name = "aurora_daily_kept_5_weeks" + schedule = "cron(0 0 * * ? *)" + lifecycle = { + delete_after = 35 + } + copy_action = { + delete_after = 365 + } + }, + { + name = "aurora_weekly_kept_3_months" + schedule = "cron(0 1 ? * SUN *)" + lifecycle = { + delete_after = 90 + } + copy_action = { + delete_after = 365 + } + }, + { + name = "aurora_monthly_kept_7_years" + schedule = "cron(0 2 1 * ? *)" + lifecycle = { + cold_storage_after = 30 + delete_after = 2555 + } + copy_action = { + delete_after = 365 + } + } + ] + } +} + +variable "iam_role_permissions_boundary" { + description = "Optional permissions boundary ARN for backup role" + type = string + default = "" # Empty by default +} From b202d723d84fd56619e7568f5045cc42e2e1a130 Mon Sep 17 00:00:00 2001 From: NIcholas Staples Date: Thu, 3 Jul 2025 15:44:23 +0100 Subject: [PATCH 2/8] Merge mistake --- modules/aws-backup-source/locals.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/aws-backup-source/locals.tf b/modules/aws-backup-source/locals.tf index d078747..8f8eed7 100644 --- a/modules/aws-backup-source/locals.tf +++ b/modules/aws-backup-source/locals.tf @@ -10,7 +10,7 @@ locals { [aws_backup_framework.main.arn], var.backup_plan_config_ebsvol.enable ? [aws_backup_framework.ebsvol[0].arn] : [], var.backup_plan_config_dynamodb.enable ? [aws_backup_framework.dynamodb[0].arn] : [], - var.backup_plan_config_aurora.enable ? aws_backup_framework.aurora[0].arn : "" + var.backup_plan_config_aurora.enable ? [aws_backup_framework.aurora[0].arn] : [] )) aurora_overrides = jsondecode(var.backup_plan_config_aurora.restore_testing_overrides) } From 536d78aa2de168a603c860dff142c675fc9cde87 Mon Sep 17 00:00:00 2001 From: davidhallam4-nhs <110543996+davidhallam4-nhs@users.noreply.github.com> Date: Mon, 2 Jun 2025 13:14:43 +0100 Subject: [PATCH 3/8] Fix cyclic kms key policy. Wildcard resource as this policy is scoped to the key itself. --- modules/aws-backup-source/kms.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/aws-backup-source/kms.tf b/modules/aws-backup-source/kms.tf index bf047fc..002540e 100644 --- a/modules/aws-backup-source/kms.tf +++ b/modules/aws-backup-source/kms.tf @@ -51,6 +51,6 @@ data "aws_iam_policy_document" "backup_key_policy" { "kms:ListGrants", "kms:DescribeKey" ] - resources = [aws_kms_key.aws_backup_key.arn] + resources = ["*"] } } From 2d875879cde19737eddf0f1e4e88e0f2b1b08ce9 Mon Sep 17 00:00:00 2001 From: Megan Date: Tue, 25 Mar 2025 14:18:25 +0000 Subject: [PATCH 4/8] mebo4-report-bucket-policy-update Add kms permissions for aws backup service role to send notification to sns topic --- examples/source/aws-backups.tf | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/examples/source/aws-backups.tf b/examples/source/aws-backups.tf index 3876268..d4a59ff 100644 --- a/examples/source/aws-backups.tf +++ b/examples/source/aws-backups.tf @@ -100,6 +100,14 @@ resource "aws_kms_key" "backup_notifications" { Action = ["kms:GenerateDataKey*", "kms:Decrypt"] Resource = "*" }, + { + Effect = "Allow" + Principal = { + Service = "backup.amazonaws.com" + } + Action = ["kms:GenerateDataKey*", "kms:Decrypt"] + Resource = "*" + }, ] }) } From 7bf0b5492273b85512f00f7bb0b4e48f9c3952ce Mon Sep 17 00:00:00 2001 From: Jon Marston Date: Tue, 3 Jun 2025 15:13:03 +0100 Subject: [PATCH 5/8] EM-1690: replace the current user ARN with the terraform role variable value. --- modules/aws-backup-source/kms.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/aws-backup-source/kms.tf b/modules/aws-backup-source/kms.tf index 002540e..41013db 100644 --- a/modules/aws-backup-source/kms.tf +++ b/modules/aws-backup-source/kms.tf @@ -31,7 +31,7 @@ data "aws_iam_policy_document" "backup_key_policy" { sid = "EnableIAMUserPermissions" principals { type = "AWS" - identifiers = ["arn:aws:iam::${data.aws_caller_identity.current.account_id}:root", data.aws_caller_identity.current.arn] + identifiers = ["arn:aws:iam::${data.aws_caller_identity.current.account_id}:root", var.terraform_role_arn] } actions = ["kms:*"] resources = ["*"] From 8a012aaa01f0ee7f248d810950049535a3cde6ef Mon Sep 17 00:00:00 2001 From: staplesn Date: Tue, 15 Jul 2025 16:01:59 +0100 Subject: [PATCH 6/8] Fix the interface passing completion.window instead of completion.widow --- modules/aws-backup-source/variables.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/aws-backup-source/variables.tf b/modules/aws-backup-source/variables.tf index d2089ce..2125798 100644 --- a/modules/aws-backup-source/variables.tf +++ b/modules/aws-backup-source/variables.tf @@ -161,7 +161,7 @@ variable "backup_plan_config_dynamodb" { rules = optional(list(object({ name = string schedule = string - completion_widow = optional(number) + completion_window = optional(number) enable_continuous_backup = optional(bool) lifecycle = object({ delete_after = number From 2ed7e23f4d198f57c798d0224b508b8b3b6aced7 Mon Sep 17 00:00:00 2001 From: Alex Young Date: Wed, 16 Jul 2025 10:36:22 +0100 Subject: [PATCH 7/8] Add a CHANGELOG.md --- CHANGELOG.md | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..3b5bdcb --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,46 @@ +# Changelog + +## [v1.2.0] (2025-07-10) + +### Features + +- Document s3/dynamo terraform state, simplify example (#22) +- Update copyright year (#23) +- Add open source collateral (#24) +- Add a CODEOWNERS file (#27) +- Allow arbitrary tags when selecting resources for backup (#33) +- namespace resources to allow more than one vault per source account (#34) +- EBS support (#39) +- Add completion window option to the backup plan (#41) + +### Bug Fixes + +- Resolved SNS notification KMS permissions for AWS Backup service role (#29) +- Give rights for AWSServiceRoleForBackupReports to write to the report bucket (#28) +- Specify terraform and provider minimum versions (#34) +- Reduce churn in report plans (#38) +- Grant KMS permissions for RDS cross-account copies (#40) +- Fixed cyclic KMS key policy by using wildcard resource scoping (#44) +- Reduce backup policy churn from temporary roles (#47) + +## [v1.1.0] (2024-09-26) + +### Features + +- Complete module renaming (`modules/source` → `modules/aws-backup-source`, etc.) for clarity +- Added fully worked example with passed-in resources in `examples/aws-backups.tf` +- Added S3 bucket versioning requirements documentation + +- ### Bug Fixes + +- Removed v1.1.0 signposts from documentation +- Fixed cyclic KMS key policy issue + +### Documentation + +- Major README.md rewrite with clearer structure and usage instruction +- Added detailed backup/restore testing guidance and procedural notes + +## v1.0.0 + +Initial prerelease. Not for direct consumption. From 47a982eeea9f66d36e099de7564fecc60ba628d6 Mon Sep 17 00:00:00 2001 From: Ian Ridehalgh Date: Mon, 24 Mar 2025 15:46:13 +0000 Subject: [PATCH 8/8] add aurora support --- examples/source/aws-backups.tf | 8 -------- modules/aws-backup-source/variables.tf | 4 ++-- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/examples/source/aws-backups.tf b/examples/source/aws-backups.tf index d4a59ff..3876268 100644 --- a/examples/source/aws-backups.tf +++ b/examples/source/aws-backups.tf @@ -100,14 +100,6 @@ resource "aws_kms_key" "backup_notifications" { Action = ["kms:GenerateDataKey*", "kms:Decrypt"] Resource = "*" }, - { - Effect = "Allow" - Principal = { - Service = "backup.amazonaws.com" - } - Action = ["kms:GenerateDataKey*", "kms:Decrypt"] - Resource = "*" - }, ] }) } diff --git a/modules/aws-backup-source/variables.tf b/modules/aws-backup-source/variables.tf index 2125798..e4ccfc4 100644 --- a/modules/aws-backup-source/variables.tf +++ b/modules/aws-backup-source/variables.tf @@ -161,7 +161,7 @@ variable "backup_plan_config_dynamodb" { rules = optional(list(object({ name = string schedule = string - completion_window = optional(number) + completion_widow = optional(number) enable_continuous_backup = optional(bool) lifecycle = object({ delete_after = number @@ -285,7 +285,7 @@ variable "iam_role_permissions_boundary" { type = string default = "" # Empty by default } - + variable "backup_plan_config_ebsvol" { description = "Configuration for backup plans with EBS" type = object({