diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 2484993..dfe1f01 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -10,7 +10,7 @@ on: env: PYTHON_VERSION: "3.10" TERRAFORM_DOCS_VERSION: "v0.16.0" - TFLINT_VERSION: "v0.40.1" + TFLINT_VERSION: "v0.45.0" permissions: contents: read diff --git a/.github/workflows/validate.yaml b/.github/workflows/validate.yaml index e396e4b..46f6e7a 100644 --- a/.github/workflows/validate.yaml +++ b/.github/workflows/validate.yaml @@ -49,4 +49,4 @@ jobs: - name: Terraform Validate id: validate - run: terraform validate + run: terraform -chdir=examples validate diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2f4c9dd..d4a85b6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,6 +15,7 @@ repos: - id: terraform_fmt - id: terraform_tflint - id: terraform_validate + exclude: '^[^/]+$' - id: terraform_checkov - id: terraform_docs args: diff --git a/.tflint.hcl b/.tflint.hcl index 372282e..00fa5c5 100644 --- a/.tflint.hcl +++ b/.tflint.hcl @@ -1,6 +1,6 @@ plugin "terraform" { enabled = true - version = "0.1.1" + version = "0.2.2" source = "github.com/terraform-linters/tflint-ruleset-terraform" preset = "recommended" } diff --git a/README.md b/README.md index d75996d..5e0f62d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# AWS <$module-name> Terraform module +# AWS Backup Terraform module [](https://lablabs.io/) @@ -6,44 +6,82 @@ We help companies build, run, deploy and scale software and infrastructure by em --- -[![Terraform validate](https://github.com/lablabs/terraform-aws-<$module-name>/actions/workflows/validate.yaml/badge.svg)](https://github.com/lablabs/terraform-aws-<$module-name>/actions/workflows/validate.yaml) -[![pre-commit](https://github.com/lablabs/terraform-aws-<$module-name>/actions/workflows/pre-commit.yml/badge.svg)](https://github.com/lablabs/terraform-aws-<$module-name>/actions/workflows/pre-commit.yml) +[![Terraform validate](https://github.com/lablabs/terraform-aws-backup/actions/workflows/validate.yaml/badge.svg)](https://github.com/lablabs/terraform-aws-backup/actions/workflows/validate.yaml) +[![pre-commit](https://github.com/lablabs/terraform-aws-backup/actions/workflows/pre-commit.yml/badge.svg)](https://github.com/lablabs/terraform-aws-backup/actions/workflows/pre-commit.yml) ## Description -A Terraform module to provision <$module-name> +A Terraform module to provision AWS Backup ## Related Projects Check out other [terraform modules](https://github.com/orgs/lablabs/repositories?q=terraform-aws&type=public&language=&sort=). ## Examples - -See [Basic example](examples/basic/README.md) for further information. +- [Single account example](examples/single-account/README.md) +- [Cross account example](examples/cross-account/README.md) + - Backup vault in other account will be created ## Requirements | Name | Version | |------|---------| -| [terraform](#requirement\_terraform) | >= 1.0 | +| [terraform](#requirement\_terraform) | >= 1.3 | | [aws](#requirement\_aws) | >= 4.19.0 | ## Modules -No modules. +| Name | Source | Version | +|------|--------|---------| +| [source\_kms\_key](#module\_source\_kms\_key) | cloudposse/kms-key/aws | 0.12.1 | +| [source\_label](#module\_source\_label) | cloudposse/label/null | 0.25.0 | +| [source\_role](#module\_source\_role) | cloudposse/iam-role/aws | 0.17.0 | +| [target\_kms\_key](#module\_target\_kms\_key) | cloudposse/kms-key/aws | 0.12.1 | +| [target\_label](#module\_target\_label) | cloudposse/label/null | 0.25.0 | +| [target\_role](#module\_target\_role) | cloudposse/iam-role/aws | 0.17.0 | ## Resources -No resources. +| Name | Type | +|------|------| +| [aws_backup_plan.source](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/backup_plan) | resource | +| [aws_backup_selection.source](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/backup_selection) | resource | +| [aws_backup_selection.tag](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/backup_selection) | resource | +| [aws_backup_vault.source](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/backup_vault) | resource | +| [aws_backup_vault.target](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/backup_vault) | resource | +| [aws_backup_vault_policy.source](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/backup_vault_policy) | resource | +| [aws_backup_vault_policy.target](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/backup_vault_policy) | resource | +| [aws_caller_identity.source](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | +| [aws_caller_identity.target](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | +| [aws_iam_policy_document.kms_source_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.kms_target_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.source_vault](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.target_vault](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | ## Inputs -No inputs. +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [backup\_plans](#input\_backup\_plans) | Backup plans config along with rule and resources setup |
list(object({
name = string
resources = optional(list(string), [])
selection_tags = optional(list(object({
type = string
key = string
value = string
})), [])
rules = list(object({
name = string
schedule = string
enable_continuous_backup = optional(bool)
start_window = optional(string, 60)
completion_window = optional(number, 180)
lifecycle = optional(object({
cold_storage_after = optional(number)
delete_after = optional(number)
}))
copy_action_lifecycle = optional(object({
cold_storage_after = optional(number)
delete_after = optional(number)
}))
recovery_point_tags = optional(map(string))
}))
advanced_backup_setting = optional(object({
WindowsVSS = optional(string, null)
resource_type = optional(string, null)
}), null)
}))
| `[]` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [enabled](#input\_enabled) | Variable indicating whether deployment is enabled | `bool` | `true` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [is\_cross\_account\_backup\_enabled](#input\_is\_cross\_account\_backup\_enabled) | Create backup vault on different account and turn on copy action to this vault (provider.target needs to be set) | `bool` | `false` | no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | ## Outputs -No outputs. +| Name | Description | +|------|-------------| +| [source\_backup\_vault\_arn](#output\_source\_backup\_vault\_arn) | Backup Vault ARN of source backup vault | +| [source\_backup\_vault\_id](#output\_source\_backup\_vault\_id) | Backup Vault ID of source backup vault | +| [target\_backup\_vault\_arn](#output\_target\_backup\_vault\_arn) | Backup Vault ARN of target backup vault | +| [target\_backup\_vault\_id](#output\_target\_backup\_vault\_id) | Backup Vault ID of target backup vault | ## Contributing and reporting issues diff --git a/aws_backup.tf b/aws_backup.tf new file mode 100644 index 0000000..8f071d9 --- /dev/null +++ b/aws_backup.tf @@ -0,0 +1,130 @@ +# Source vault +resource "aws_backup_vault" "source" { + count = var.enabled ? 1 : 0 + provider = aws.source + name = module.source_label.id + kms_key_arn = module.source_kms_key.key_arn + tags = module.source_label.tags + force_destroy = true +} + +resource "aws_backup_vault_policy" "source" { + count = var.enabled ? 1 : 0 + provider = aws.source + backup_vault_name = aws_backup_vault.source[0].name + policy = data.aws_iam_policy_document.source_vault.json +} + +resource "aws_backup_plan" "source" { + provider = aws.source + for_each = { for bp in var.backup_plans : bp.name => bp if var.enabled } + + name = each.value.name + tags = module.source_label.tags + + dynamic "rule" { + for_each = each.value.rules + content { + rule_name = rule.value.name + target_vault_name = aws_backup_vault.source[0].name + schedule = rule.value.schedule + start_window = try(rule.value.start_window, 60) + completion_window = try(rule.value.completion_window, 180) + recovery_point_tags = try(rule.value.recovery_point_tags, null) + enable_continuous_backup = try(rule.value.enable_continuous_backup, null) + + + dynamic "lifecycle" { + for_each = try(rule.value.lifecycle, null) != null ? [true] : [] + content { + cold_storage_after = try(rule.value.lifecycle.cold_storage_after, null) + delete_after = try(rule.value.lifecycle.delete_after, null) + } + } + + dynamic "copy_action" { + for_each = var.is_cross_account_backup_enabled == true ? [true] : [] + content { + dynamic "lifecycle" { + for_each = try(rule.value.copy_action_lifecycle, null) != null ? [true] : [] + content { + cold_storage_after = try(rule.value.copy_action_lifecycle.cold_storage_after, null) + delete_after = try(rule.value.copy_action_lifecycle.delete_after, null) + } + } + destination_vault_arn = aws_backup_vault.target[0].arn + } + + } + } + } + + dynamic "advanced_backup_setting" { + for_each = try(each.value.advanced_backup_setting, null) != null ? [true] : [] + + content { + backup_options = { + WindowsVSS = try(each.value.advanced_backup_setting.WindowsVSS, null) + } + resource_type = try(each.value.advanced_backup_setting.resource_type, null) + } + } +} + +# Resource selection by arn +resource "aws_backup_selection" "source" { + for_each = { for bp in flatten([ + for bp_plan in var.backup_plans : [ + for resource in bp_plan.resources : { + backup_plan_key : bp_plan.name + resource_arn : resource + } + ] + ]) : md5("${bp.backup_plan_key}${bp.resource_arn}") => bp if var.enabled } + + provider = aws.source + iam_role_arn = module.source_role.arn + plan_id = aws_backup_plan.source[each.value.backup_plan_key].id + name = substr("${module.source_label.id}-${each.key}", 0, 50) + resources = [each.value.resource_arn] +} + +# Resource selection by tag +resource "aws_backup_selection" "tag" { + for_each = { for bp in flatten([ + for bp_plan in var.backup_plans : [ + for selection_tag in bp_plan.selection_tags : { + backup_plan_key : bp_plan.name + selection_tag : selection_tag + } + ] + ]) : md5("${bp.backup_plan_key}${bp.selection_tag["type"]}${bp.selection_tag["key"]}${bp.selection_tag["value"]}") => bp if var.enabled } + + provider = aws.source + iam_role_arn = module.source_role.arn + plan_id = aws_backup_plan.source[each.value.backup_plan_key].id + name = substr("${module.source_label.id}-${each.key}", 0, 50) + resources = ["*"] + selection_tag { + type = each.value.selection_tag["type"] + key = each.value.selection_tag["key"] + value = each.value.selection_tag["value"] + } +} + +# Target vault +resource "aws_backup_vault" "target" { + count = var.enabled && var.is_cross_account_backup_enabled ? 1 : 0 + provider = aws.target + name = module.target_label.id + kms_key_arn = module.target_kms_key.key_arn + tags = module.source_label.tags + force_destroy = true +} + +resource "aws_backup_vault_policy" "target" { + count = var.enabled && var.is_cross_account_backup_enabled ? 1 : 0 + provider = aws.target + backup_vault_name = aws_backup_vault.target[0].name + policy = data.aws_iam_policy_document.target_vault[0].json +} diff --git a/context.tf b/context.tf new file mode 100644 index 0000000..e6a5bb3 --- /dev/null +++ b/context.tf @@ -0,0 +1,94 @@ +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} diff --git a/examples/basic/main.tf b/examples/basic/main.tf deleted file mode 100644 index 838b218..0000000 --- a/examples/basic/main.tf +++ /dev/null @@ -1,3 +0,0 @@ -module "example_module" { - source = "../../" -} diff --git a/examples/basic/providers.tf b/examples/basic/providers.tf deleted file mode 100644 index e1bf6a7..0000000 --- a/examples/basic/providers.tf +++ /dev/null @@ -1,3 +0,0 @@ -provider "aws" { - region = "eu-central-1" -} diff --git a/examples/basic/README.md b/examples/cross-account/README.md similarity index 56% rename from examples/basic/README.md rename to examples/cross-account/README.md index b4eaa18..863c119 100644 --- a/examples/basic/README.md +++ b/examples/cross-account/README.md @@ -14,11 +14,14 @@ The code in this example shows how to use the module with basic configuration an | Name | Source | Version | |------|--------|---------| -| [example\_module](#module\_example\_module) | ../../ | n/a | +| [aws-backup-dev-audit](#module\_aws-backup-dev-audit) | ../../ | n/a | ## Resources -No resources. +| Name | Type | +|------|------| +| [aws_backup_global_settings.aws_backup](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/backup_global_settings) | resource | +| [aws_dynamodb_table.example](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/dynamodb_table) | resource | ## Inputs diff --git a/examples/cross-account/base.tf b/examples/cross-account/base.tf new file mode 100644 index 0000000..a40e44f --- /dev/null +++ b/examples/cross-account/base.tf @@ -0,0 +1,30 @@ +# terraform apply --target aws_dynamodb_table.basic-dynamodb-table +resource "aws_dynamodb_table" "example" { + #checkov:skip=CKV_AWS_28 + #checkov:skip=CKV_AWS_119 + #checkov:skip=CKV2_AWS_16 + #checkov:skip=CKV_AWS_28 + + + provider = aws.source + name = "TestAWSBackup" + read_capacity = 1 + write_capacity = 1 + hash_key = "UserId" + range_key = "Title" + + attribute { + name = "UserId" + type = "S" + } + + attribute { + name = "Title" + type = "S" + } + + tags = { + Name = "TestAWSBackup" + Environment = "Dev" + } +} diff --git a/examples/cross-account/main.tf b/examples/cross-account/main.tf new file mode 100644 index 0000000..764339f --- /dev/null +++ b/examples/cross-account/main.tf @@ -0,0 +1,77 @@ +resource "aws_backup_global_settings" "aws_backup" { + global_settings = { + "isCrossAccountBackupEnabled" = "true" + } + provider = aws.management +} + +module "aws-backup-dev-audit" { + source = "../../" + + providers = { + aws.source = aws.source + aws.target = aws.target + } + + enabled = true + + name = "dynamod-db" + + # namespace = "aws-backup" + + is_cross_account_backup_enabled = true + + backup_plans = [ + { + name = "dynamodb-plan" + resources = [aws_dynamodb_table.example.arn] + selection_tags = [ + { + type = "STRINGEQUALS" + key = "tag:stage" + value = "dev" + } + ] + rules = [{ + name = "dynamodb-rule" + schedule = "cron(30 * * * ? *)" + start_window = 60 + completion_window = 120 + lifecycle = { + delete_after = 14 + } + copy_action_lifecycle = { + delete_after = 14 + } + + recovery_point_tags = { + "Environment" = "dev" + } + }, + { + name = "dynamodb-rule" + schedule = "cron(0 * * * ? *)" + start_window = 60 + completion_window = 120 + lifecycle = { + delete_after = 14 + } + recovery_point_tags = { + "Environment" = "dev" + } + }] + }, + { + name = "dynamodb-plan2" + resources = [aws_dynamodb_table.example.arn] + rules = [{ + name = "dynamodb-rule2" + schedule = "cron(0 1 * * ? *)" + start_window = 120 + completion_window = 360 + }] + } + ] + + +} diff --git a/examples/cross-account/providers.tf b/examples/cross-account/providers.tf new file mode 100644 index 0000000..670611d --- /dev/null +++ b/examples/cross-account/providers.tf @@ -0,0 +1,17 @@ +provider "aws" { + alias = "management" + region = "eu-central-1" + profile = "management" +} + +provider "aws" { + alias = "source" + region = "eu-central-1" + profile = "dev" +} + +provider "aws" { + alias = "target" + region = "eu-central-1" + profile = "audit" +} diff --git a/examples/basic/versions.tf b/examples/cross-account/versions.tf similarity index 99% rename from examples/basic/versions.tf rename to examples/cross-account/versions.tf index 90a7444..3b89985 100644 --- a/examples/basic/versions.tf +++ b/examples/cross-account/versions.tf @@ -5,6 +5,7 @@ terraform { aws = { source = "hashicorp/aws" version = ">= 4.19.0" + } } } diff --git a/examples/single-account/README.md b/examples/single-account/README.md new file mode 100644 index 0000000..863c119 --- /dev/null +++ b/examples/single-account/README.md @@ -0,0 +1,33 @@ +# Basic example + +The code in this example shows how to use the module with basic configuration and minimal set of other resources. + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0 | +| [aws](#requirement\_aws) | >= 4.19.0 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [aws-backup-dev-audit](#module\_aws-backup-dev-audit) | ../../ | n/a | + +## Resources + +| Name | Type | +|------|------| +| [aws_backup_global_settings.aws_backup](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/backup_global_settings) | resource | +| [aws_dynamodb_table.example](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/dynamodb_table) | resource | + +## Inputs + +No inputs. + +## Outputs + +No outputs. + diff --git a/examples/single-account/base.tf b/examples/single-account/base.tf new file mode 100644 index 0000000..a40e44f --- /dev/null +++ b/examples/single-account/base.tf @@ -0,0 +1,30 @@ +# terraform apply --target aws_dynamodb_table.basic-dynamodb-table +resource "aws_dynamodb_table" "example" { + #checkov:skip=CKV_AWS_28 + #checkov:skip=CKV_AWS_119 + #checkov:skip=CKV2_AWS_16 + #checkov:skip=CKV_AWS_28 + + + provider = aws.source + name = "TestAWSBackup" + read_capacity = 1 + write_capacity = 1 + hash_key = "UserId" + range_key = "Title" + + attribute { + name = "UserId" + type = "S" + } + + attribute { + name = "Title" + type = "S" + } + + tags = { + Name = "TestAWSBackup" + Environment = "Dev" + } +} diff --git a/examples/single-account/main.tf b/examples/single-account/main.tf new file mode 100644 index 0000000..1afdeec --- /dev/null +++ b/examples/single-account/main.tf @@ -0,0 +1,52 @@ +resource "aws_backup_global_settings" "aws_backup" { + global_settings = { + "isCrossAccountBackupEnabled" = "true" + } + provider = aws.management +} + +module "aws-backup-dev-audit" { + source = "../../" + + providers = { + aws.source = aws.source + aws.target = aws.source + } + + name = "dynamod-db" + + namespace = "aws-backup" + + is_cross_account_backup_enabled = false + + backup_plans = [ + { + name = "dynamodb-plan" + resources = [aws_dynamodb_table.example.arn] + rules = [{ + name = "dynamodb-rule" + schedule = "cron(0 1 * * ? *)" + start_window = 120 + completion_window = 360 + lifecycle = { + delete_after = 14 + } + recovery_point_tags = { + "Environment" = "dev" + } + }] + }, + { + name = "dynamodb-plan2" + resources = [aws_dynamodb_table.example.arn] + rules = [{ + name = "dynamodb-rule2" + schedule = "cron(0 1 * * ? *)" + start_window = 120 + completion_window = 360 + }] + } + ] + + +} diff --git a/examples/single-account/providers.tf b/examples/single-account/providers.tf new file mode 100644 index 0000000..d537219 --- /dev/null +++ b/examples/single-account/providers.tf @@ -0,0 +1,11 @@ +provider "aws" { + alias = "management" + region = "eu-central-1" + profile = "management" +} + +provider "aws" { + alias = "source" + region = "eu-central-1" + profile = "dev" +} diff --git a/examples/single-account/versions.tf b/examples/single-account/versions.tf new file mode 100644 index 0000000..3b89985 --- /dev/null +++ b/examples/single-account/versions.tf @@ -0,0 +1,11 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.19.0" + + } + } +} diff --git a/iam.tf b/iam.tf new file mode 100644 index 0000000..577841d --- /dev/null +++ b/iam.tf @@ -0,0 +1,106 @@ +locals { + aws_backup_managed_policy_arns = [ + "arn:aws:iam::aws:policy/service-role/AWSBackupServiceRolePolicyForBackup", + "arn:aws:iam::aws:policy/service-role/AWSBackupServiceRolePolicyForRestores" + ] + role_description = "Provides AWS Backup permission to create backups and perform restores on your behalf across AWS services" +} + +module "source_role" { + source = "cloudposse/iam-role/aws" + version = "0.17.0" + + enabled = var.enabled + providers = { + aws = aws.source + } + + context = module.source_label.context + + role_description = local.role_description + + assume_role_actions = [ + "sts:AssumeRole" + ] + + principals = { + Service = ["backup.amazonaws.com"] + } + + managed_policy_arns = local.aws_backup_managed_policy_arns + + policy_document_count = 0 +} + + +module "target_role" { + source = "cloudposse/iam-role/aws" + version = "0.17.0" + + enabled = var.enabled && var.is_cross_account_backup_enabled + providers = { + aws = aws.target + } + + context = module.target_label.context + + role_description = local.role_description + + assume_role_actions = [ + "sts:AssumeRole" + ] + + principals = { + Service = ["backup.amazonaws.com"] + } + + policy_document_count = 0 + + managed_policy_arns = local.aws_backup_managed_policy_arns +} + + +# aws backup + +data "aws_iam_policy_document" "source_vault" { + provider = aws.source + + statement { + sid = "Enable backup" + effect = "Allow" + + actions = ["backup:CopyIntoBackupVault"] + + #checkov:skip=CKV_AWS_109 + resources = ["*"] + + principals { + identifiers = [ + "arn:aws:iam::${data.aws_caller_identity.target.account_id}:root", + ] + type = "AWS" + } + } +} + +data "aws_iam_policy_document" "target_vault" { + count = var.is_cross_account_backup_enabled ? 1 : 0 + provider = aws.target + + statement { + sid = "Enable backup" + effect = "Allow" + + actions = ["backup:CopyIntoBackupVault"] + + #checkov:skip=CKV_AWS_109 + resources = ["*"] + + principals { + identifiers = [ + "arn:aws:iam::${data.aws_caller_identity.source.account_id}:root", + ] + type = "AWS" + } + } +} diff --git a/kms.tf b/kms.tf new file mode 100644 index 0000000..1513187 --- /dev/null +++ b/kms.tf @@ -0,0 +1,125 @@ +module "source_kms_key" { + source = "cloudposse/kms-key/aws" + version = "0.12.1" + + enabled = var.enabled + providers = { + aws = aws.source + } + + context = module.source_label.context + deletion_window_in_days = 7 + enable_key_rotation = true + alias = "alias/${module.source_label.id}" + policy = data.aws_iam_policy_document.kms_source_policy.json +} + +module "target_kms_key" { + source = "cloudposse/kms-key/aws" + version = "0.12.1" + + enabled = var.enabled && var.is_cross_account_backup_enabled + providers = { + aws = aws.target + } + + context = module.target_label.context + deletion_window_in_days = 7 + enable_key_rotation = true + alias = "alias/${module.target_label.id}" + policy = data.aws_iam_policy_document.kms_target_policy.json +} + +data "aws_caller_identity" "source" { + provider = aws.source +} + +data "aws_caller_identity" "target" { + provider = aws.target +} + +data "aws_iam_policy_document" "kms_source_policy" { + provider = aws.source + statement { + sid = "Enable IAM User Permissions" + effect = "Allow" + + #checkov:skip=CKV_AWS_111 + actions = ["kms:*"] + + #checkov:skip=CKV_AWS_109 + resources = ["*"] + + principals { + identifiers = [ + "arn:aws:iam::${data.aws_caller_identity.source.account_id}:root", + ] + type = "AWS" + } + } + statement { + sid = "Allow use of the key" + effect = "Allow" + + #checkov:skip=CKV_AWS_111 + actions = ["kms:Encrypt", + "kms:Decrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*", + "kms:DescribeKey" + ] + + #checkov:skip=CKV_AWS_109 + resources = ["*"] + + principals { + identifiers = [ + "arn:aws:iam::${data.aws_caller_identity.target.account_id}:root", + ] + type = "AWS" + } + } +} + +data "aws_iam_policy_document" "kms_target_policy" { + provider = aws.target + statement { + sid = "Enable IAM User Permissions" + effect = "Allow" + + #checkov:skip=CKV_AWS_111 + actions = ["kms:*"] + + #checkov:skip=CKV_AWS_109 + resources = ["*"] + + principals { + identifiers = [ + "arn:aws:iam::${data.aws_caller_identity.target.account_id}:root" + ] + type = "AWS" + } + } + + statement { + sid = "Allow use of the key" + effect = "Allow" + + actions = ["kms:Encrypt", + "kms:Decrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*", + "kms:DescribeKey" + ] + + #checkov:skip=CKV_AWS_109 + resources = ["*"] + + principals { + identifiers = [ + "arn:aws:iam::${data.aws_caller_identity.source.account_id}:root", + ] + type = "AWS" + } + } +} diff --git a/label.tf b/label.tf new file mode 100644 index 0000000..15a352a --- /dev/null +++ b/label.tf @@ -0,0 +1,23 @@ +module "source_label" { + source = "cloudposse/label/null" + version = "0.25.0" + name = var.name + namespace = var.namespace + stage = var.stage + attributes = concat(["source"], var.attributes) + environment = var.environment + tags = var.tags + context = var.context +} + +module "target_label" { + source = "cloudposse/label/null" + version = "0.25.0" + name = var.name + namespace = var.namespace + stage = var.stage + attributes = concat(["target"], var.attributes) + environment = var.environment + tags = var.tags + context = var.context +} diff --git a/main.tf b/main.tf deleted file mode 100644 index e69de29..0000000 diff --git a/outputs.tf b/outputs.tf index e69de29..25fd1dd 100644 --- a/outputs.tf +++ b/outputs.tf @@ -0,0 +1,19 @@ +output "source_backup_vault_id" { + value = try(aws_backup_vault.source[0].id, "") + description = "Backup Vault ID of source backup vault" +} + +output "source_backup_vault_arn" { + value = try(aws_backup_vault.source[0].arn, "") + description = "Backup Vault ARN of source backup vault" +} + +output "target_backup_vault_id" { + value = try(aws_backup_vault.target[0].id, "") + description = "Backup Vault ID of target backup vault" +} + +output "target_backup_vault_arn" { + value = try(aws_backup_vault.target[0].arn, "") + description = "Backup Vault ARN of target backup vault" +} diff --git a/variables.tf b/variables.tf index e69de29..25fbb56 100644 --- a/variables.tf +++ b/variables.tf @@ -0,0 +1,45 @@ +variable "enabled" { + type = bool + default = true + description = "Variable indicating whether deployment is enabled" +} + +variable "is_cross_account_backup_enabled" { + type = bool + default = false + description = "Create backup vault on different account and turn on copy action to this vault (provider.target needs to be set)" +} + +variable "backup_plans" { + default = [] + description = "Backup plans config along with rule and resources setup" + type = list(object({ + name = string + resources = optional(list(string), []) + selection_tags = optional(list(object({ + type = string + key = string + value = string + })), []) + rules = list(object({ + name = string + schedule = string + enable_continuous_backup = optional(bool) + start_window = optional(string, 60) + completion_window = optional(number, 180) + lifecycle = optional(object({ + cold_storage_after = optional(number) + delete_after = optional(number) + })) + copy_action_lifecycle = optional(object({ + cold_storage_after = optional(number) + delete_after = optional(number) + })) + recovery_point_tags = optional(map(string)) + })) + advanced_backup_setting = optional(object({ + WindowsVSS = optional(string, null) + resource_type = optional(string, null) + }), null) + })) +} diff --git a/versions.tf b/versions.tf index 90a7444..c395a0a 100644 --- a/versions.tf +++ b/versions.tf @@ -1,10 +1,11 @@ terraform { - required_version = ">= 1.0" + required_version = ">= 1.3" required_providers { aws = { - source = "hashicorp/aws" - version = ">= 4.19.0" + source = "hashicorp/aws" + version = ">= 4.19.0" + configuration_aliases = [aws.source, aws.target] } } }