From 8ced5a75a63f231bb61cbc6d27eaf599041ec2f6 Mon Sep 17 00:00:00 2001 From: John Collinson <13622412+johncollinson2001@users.noreply.github.com> Date: Wed, 18 Sep 2024 10:17:42 +0100 Subject: [PATCH] Terraform tests (#8) * Added terraform HCL integration tests and integrated test execution into CI pipeline. * Removed superfluous comments from tf tests. --- .github/workflows/ci-pipeline.yaml | 10 ++- README.md | 85 +++++++++++++++++-- infrastructure/backup_policy.tf | 16 ++++ infrastructure/backup_vault.tf | 11 +++ infrastructure/main.tf | 41 --------- .../backup_policy/blob_storage/output.tf | 12 +++ .../backup_policy/managed_disk/output.tf | 16 ++++ infrastructure/resource_group.tf | 4 + .../integration-tests/azurerm/data.tfmock.hcl | 5 ++ .../backup_policy.tftest.hcl | 78 +++++++++++++++++ .../integration-tests/backup_vault.tftest.hcl | 58 +++++++++++++ tests/integration-tests/main.tf | 8 ++ .../resource_group.tftest.hcl | 32 +++++++ tests/integration-tests/setup/main.tf | 16 ++++ 14 files changed, 340 insertions(+), 52 deletions(-) create mode 100644 infrastructure/backup_policy.tf create mode 100644 infrastructure/backup_vault.tf create mode 100644 infrastructure/resource_group.tf create mode 100644 tests/integration-tests/azurerm/data.tfmock.hcl create mode 100644 tests/integration-tests/backup_policy.tftest.hcl create mode 100644 tests/integration-tests/backup_vault.tftest.hcl create mode 100644 tests/integration-tests/main.tf create mode 100644 tests/integration-tests/resource_group.tftest.hcl create mode 100644 tests/integration-tests/setup/main.tf diff --git a/.github/workflows/ci-pipeline.yaml b/.github/workflows/ci-pipeline.yaml index 4b87964..4e2b79f 100644 --- a/.github/workflows/ci-pipeline.yaml +++ b/.github/workflows/ci-pipeline.yaml @@ -24,7 +24,7 @@ jobs: - name: Install Terraform uses: hashicorp/setup-terraform@v2 with: - terraform_version: 1.5.0 + terraform_version: 1.9.3 - name: Terraform Init run: terraform init -backend=false @@ -34,6 +34,12 @@ jobs: run: terraform validate working-directory: infrastructure + - name: Run Integration Tests + run: | + terraform init -backend=false + terraform test + working-directory: tests/integration-tests + static-code-analysis: name: Static Code Analysis runs-on: ubuntu-latest @@ -45,7 +51,7 @@ jobs: - name: Install Terraform uses: hashicorp/setup-terraform@v2 with: - terraform_version: 1.5.0 + terraform_version: 1.9.3 - name: Run Terraform Format run: terraform fmt -check diff --git a/README.md b/README.md index 9290182..c5fd77d 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,12 @@ The following diagram illustrates the high level architecture The repository consists of the following directories: +* `./.github` + + Contains the GitHub workflows in `yaml` format. + + [See the YAML schema documentation for more details.](https://learn.microsoft.com/en-us/azure/devops/pipelines/yaml-schema/?view=azure-pipelines) + * `./.pipelines` Contains the Azure Pipelines in `yaml` format. @@ -86,6 +92,10 @@ The repository consists of the following directories: Contains scripts that are used to create and maintain the environment. +* `./tests` + + Contains the different types of tests used to verify the solution. + ## Developer Guide ### Environment Setup @@ -93,21 +103,19 @@ The repository consists of the following directories: The following are pre-reqs to working with the solution: * An Azure subscription -* Azure CLI installed -* Terraform installed -* An Azure identity with the following roles: - * Contributor role on the subscription (required to create resources) - * RBAC Administrator role on the resources being backed up (required to assign roles on the resource to the backup vault managed identity) +* An Azure identity assigned the subscription Contributor role (required to create resources) +* [Azure CLI installed](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli-windows?tabs=azure-cli) +* [Terraform installed](https://developer.hashicorp.com/terraform/install) -[See the following link for further information.](https://learn.microsoft.com/en-us/azure/developer/terraform/get-started-windows-powershell) +> Ensure all installed components have been added to the `%PATH%` - e.g. `az` and `terraform`. ### Getting Started -Take the following steps to get started in configuring and verify the infrastructure: +Take the following steps to get started in configuring and verifying the infrastructure for your development environment: 1. Login to Azure - Use the Azure CLI to login to Azure by running the following command: + Use Azure CLI to login to Azure by running the following command: ```pwsh az login @@ -167,6 +175,36 @@ Take the following steps to get started in configuring and verify the infrastruc The repo contains an `example` module which can be utilised to further extend the sample infrastructure with some resources and backup instances. To use this module for dev/test purposes, include the module in `main.tf` and run `terraform apply` again. +### Running the Tests + +#### Integration Tests + +The test suite consists of a number Terraform HCL integration tests that use a mock azurerm provider. + +[See this link for more information.](https://developer.hashicorp.com/terraform/language/tests) + +Take the following steps to run the test suite: + +1. Initialise Terraform + + Change the working directory to `./tests/integration-tests`. + + Terraform can now be initialised by running the following command: + + ````pwsh + terraform init -backend=false + ```` + + > NOTE: There's no need to initialise a backend for the purposes of running the tests. + +2. Run the Tests + + Run the tests with the following command: + + ````pwsh + terraform test + ```` + ### Contributing If you want to contribute to the project, raise a PR on GitHub. @@ -185,4 +223,33 @@ We use pre-commit to run analysis and checks on the changes being committed. Tak * Install pre-commit within the repository with the following command: `pre-commit install` * Run `pre-commit run --all-files` to check pre-commit is working - > For full details [see this link](https://pre-commit.com/#installation) +> For full details [see this link](https://pre-commit.com/#installation) + +## CI Pipeline + +The CI pipeline builds and verifies the solution and runs a number of static code analysis steps on the code base. + +### End to End Testing + +Part of the build verification is the end to end testing step. This requires the pipeline to login to Azure in order to deploy an environment on which to execute the tests. + +In order for the CI pipeline to login to Azure the following GitHub actions secret must be created called `AZURE_CREDENTIALS` set as a JSON object in the following structure: + +```json +{ + "clientSecret": "******", + "subscriptionId": "******", + "tenantId": "******", + "clientId": "******" +} +``` + +### Static Code Analysis + +The following static code analysis checks are executed: + +* [Terraform format](https://developer.hashicorp.com/terraform/cli/commands/fmt) +* [Terraform lint](https://github.com/terraform-linters/tflint) +* [Checkov scan](https://www.checkov.io/) +* [Gitleaks scan](https://github.com/gitleaks/gitleaks) +* [Trivy vulnerability scan](https://github.com/aquasecurity/trivy) diff --git a/infrastructure/backup_policy.tf b/infrastructure/backup_policy.tf new file mode 100644 index 0000000..a7d506e --- /dev/null +++ b/infrastructure/backup_policy.tf @@ -0,0 +1,16 @@ +module "blob_storage_policy" { + source = "./modules/backup_policy/blob_storage" + policy_name = "bkpol-${var.vault_name}-blobstorage" + vault_id = azurerm_data_protection_backup_vault.backup_vault.id + retention_period = "P7D" # 7 days + # NOTE - this blob policy has been configured for operational backup + # only, which continuously backs up data and does not need a schedule +} + +module "managed_disk_policy" { + source = "./modules/backup_policy/managed_disk" + policy_name = "bkpol-${var.vault_name}-manageddisk" + vault_id = azurerm_data_protection_backup_vault.backup_vault.id + retention_period = "P7D" # 7 days + backup_intervals = ["R/2024-01-01T00:00:00+00:00/P1D"] # Once per day at 00:00 +} diff --git a/infrastructure/backup_vault.tf b/infrastructure/backup_vault.tf new file mode 100644 index 0000000..cc212ed --- /dev/null +++ b/infrastructure/backup_vault.tf @@ -0,0 +1,11 @@ +resource "azurerm_data_protection_backup_vault" "backup_vault" { + name = "bvault-${var.vault_name}" + resource_group_name = azurerm_resource_group.resource_group.name + location = var.vault_location + datastore_type = "VaultStore" + redundancy = var.vault_redundancy + soft_delete = "Off" + identity { + type = "SystemAssigned" + } +} diff --git a/infrastructure/main.tf b/infrastructure/main.tf index 54870ac..a93ba04 100644 --- a/infrastructure/main.tf +++ b/infrastructure/main.tf @@ -11,45 +11,4 @@ terraform { provider "azurerm" { features {} -} - -resource "azurerm_resource_group" "resource_group" { - location = var.vault_location - name = "rg-nhsbackup-${var.vault_name}" -} - -# Create the vault -########################################################################### - -resource "azurerm_data_protection_backup_vault" "backup_vault" { - name = "bvault-${var.vault_name}" - resource_group_name = azurerm_resource_group.resource_group.name - location = var.vault_location - datastore_type = "VaultStore" - redundancy = var.vault_redundancy - soft_delete = "Off" - identity { - type = "SystemAssigned" - } -} - - -# Create some backup policies -########################################################################### - -module "blob_storage_policy" { - source = "./modules/backup_policy/blob_storage" - policy_name = "bkpol-${var.vault_name}-blobstorage" - vault_id = azurerm_data_protection_backup_vault.backup_vault.id - retention_period = "P7D" # 7 days - # NOTE - this blob policy has been configured for operational backup - # only, which continuously backs up data and does not need a schedule -} - -module "managed_disk_policy" { - source = "./modules/backup_policy/managed_disk" - policy_name = "bkpol-${var.vault_name}-manageddisk" - vault_id = azurerm_data_protection_backup_vault.backup_vault.id - retention_period = "P7D" # 7 days - backup_intervals = ["R/2024-01-01T00:00:00+00:00/P1D"] # Once per day at 00:00 } \ No newline at end of file diff --git a/infrastructure/modules/backup_policy/blob_storage/output.tf b/infrastructure/modules/backup_policy/blob_storage/output.tf index d6d56b0..b328c21 100644 --- a/infrastructure/modules/backup_policy/blob_storage/output.tf +++ b/infrastructure/modules/backup_policy/blob_storage/output.tf @@ -1,3 +1,15 @@ output "id" { value = azurerm_data_protection_backup_policy_blob_storage.backup_policy.id } + +output "name" { + value = azurerm_data_protection_backup_policy_blob_storage.backup_policy.name +} + +output "vault_id" { + value = azurerm_data_protection_backup_policy_blob_storage.backup_policy.vault_id +} + +output "retention_period" { + value = azurerm_data_protection_backup_policy_blob_storage.backup_policy.operational_default_retention_duration +} diff --git a/infrastructure/modules/backup_policy/managed_disk/output.tf b/infrastructure/modules/backup_policy/managed_disk/output.tf index ad5748d..986d7f8 100644 --- a/infrastructure/modules/backup_policy/managed_disk/output.tf +++ b/infrastructure/modules/backup_policy/managed_disk/output.tf @@ -1,3 +1,19 @@ output "id" { value = azurerm_data_protection_backup_policy_disk.backup_policy.id } + +output "name" { + value = azurerm_data_protection_backup_policy_disk.backup_policy.name +} + +output "vault_id" { + value = azurerm_data_protection_backup_policy_disk.backup_policy.vault_id +} + +output "retention_period" { + value = azurerm_data_protection_backup_policy_disk.backup_policy.default_retention_duration +} + +output "backup_intervals" { + value = azurerm_data_protection_backup_policy_disk.backup_policy.backup_repeating_time_intervals +} diff --git a/infrastructure/resource_group.tf b/infrastructure/resource_group.tf new file mode 100644 index 0000000..7f4fdaa --- /dev/null +++ b/infrastructure/resource_group.tf @@ -0,0 +1,4 @@ +resource "azurerm_resource_group" "resource_group" { + location = var.vault_location + name = "rg-nhsbackup-${var.vault_name}" +} diff --git a/tests/integration-tests/azurerm/data.tfmock.hcl b/tests/integration-tests/azurerm/data.tfmock.hcl new file mode 100644 index 0000000..85641aa --- /dev/null +++ b/tests/integration-tests/azurerm/data.tfmock.hcl @@ -0,0 +1,5 @@ +mock_resource "azurerm_data_protection_backup_vault" { + defaults = { + id = "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/example-resource-group/providers/Microsoft.DataProtection/backupVaults/bvault-testvault" + } +} \ No newline at end of file diff --git a/tests/integration-tests/backup_policy.tftest.hcl b/tests/integration-tests/backup_policy.tftest.hcl new file mode 100644 index 0000000..d8a1875 --- /dev/null +++ b/tests/integration-tests/backup_policy.tftest.hcl @@ -0,0 +1,78 @@ +mock_provider "azurerm" { + source = "./azurerm" +} + +run "setup_tests" { + module { + source = "./setup" + } +} + +run "create_blob_storage_policy" { + command = apply + + module { + source = "../../infrastructure" + } + + variables { + vault_name = run.setup_tests.vault_name + } + + assert { + condition = length(module.blob_storage_policy.id) > 0 + error_message = "Blob storage policy id not as expected." + } + + assert { + condition = module.blob_storage_policy.name == "bkpol-${var.vault_name}-blobstorage" + error_message = "Blob storage policy name not as expected." + } + + assert { + condition = module.blob_storage_policy.vault_id == azurerm_data_protection_backup_vault.backup_vault.id + error_message = "Blob storage policy vault id not as expected." + } + + assert { + condition = module.blob_storage_policy.retention_period == "P7D" + error_message = "Blob storage policy retention period not as expected." + } +} + +run "create_managed_disk_policy" { + command = apply + + module { + source = "../../infrastructure" + } + + variables { + vault_name = run.setup_tests.vault_name + } + + assert { + condition = length(module.managed_disk_policy.id) > 0 + error_message = "Managed disk policy id not as expected." + } + + assert { + condition = module.managed_disk_policy.name == "bkpol-${var.vault_name}-manageddisk" + error_message = "Managed disk policy name not as expected." + } + + assert { + condition = module.managed_disk_policy.vault_id == azurerm_data_protection_backup_vault.backup_vault.id + error_message = "Managed disk policy vault id not as expected." + } + + assert { + condition = module.managed_disk_policy.retention_period == "P7D" + error_message = "Managed disk policy retention period not as expected." + } + + assert { + condition = can(module.managed_disk_policy.backup_intervals) && length(module.managed_disk_policy.backup_intervals) == 1 && module.managed_disk_policy.backup_intervals[0] == "R/2024-01-01T00:00:00+00:00/P1D" + error_message = "Managed disk policy backup intervals not as expected." + } +} \ No newline at end of file diff --git a/tests/integration-tests/backup_vault.tftest.hcl b/tests/integration-tests/backup_vault.tftest.hcl new file mode 100644 index 0000000..7c08e36 --- /dev/null +++ b/tests/integration-tests/backup_vault.tftest.hcl @@ -0,0 +1,58 @@ +mock_provider "azurerm" { + source = "./azurerm" +} + +run "setup_tests" { + module { + source = "./setup" + } +} + +run "create_backup_vault" { + command = apply + + module { + source = "../../infrastructure" + } + + variables { + vault_name = run.setup_tests.vault_name + vault_location = "uksouth" + vault_redundancy = "LocallyRedundant" + } + + assert { + condition = azurerm_data_protection_backup_vault.backup_vault.name == "bvault-${var.vault_name}" + error_message = "Backup vault name not as expected." + } + + assert { + condition = azurerm_data_protection_backup_vault.backup_vault.resource_group_name == azurerm_resource_group.resource_group.name + error_message = "Resource group not as expected." + } + + assert { + condition = azurerm_data_protection_backup_vault.backup_vault.location == var.vault_location + error_message = "Backup vault location not as expected." + } + + assert { + condition = azurerm_data_protection_backup_vault.backup_vault.datastore_type == "VaultStore" + error_message = "Backup vault datastore type not as expected." + } + + assert { + condition = azurerm_data_protection_backup_vault.backup_vault.redundancy == var.vault_redundancy + error_message = "Backup vault redundancy not as expected." + } + + assert { + condition = azurerm_data_protection_backup_vault.backup_vault.soft_delete == "Off" + error_message = "Backup vault soft delete not as expected." + } + + assert { + condition = length(azurerm_data_protection_backup_vault.backup_vault.identity[0].principal_id) > 0 + error_message = "Backup vault identity not as expected." + } +} \ No newline at end of file diff --git a/tests/integration-tests/main.tf b/tests/integration-tests/main.tf new file mode 100644 index 0000000..82f3445 --- /dev/null +++ b/tests/integration-tests/main.tf @@ -0,0 +1,8 @@ +terraform { + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "3.114.0" + } + } +} diff --git a/tests/integration-tests/resource_group.tftest.hcl b/tests/integration-tests/resource_group.tftest.hcl new file mode 100644 index 0000000..75935a6 --- /dev/null +++ b/tests/integration-tests/resource_group.tftest.hcl @@ -0,0 +1,32 @@ +mock_provider "azurerm" { + source = "./azurerm" +} + +run "setup_tests" { + module { + source = "./setup" + } +} + +run "create_resource_group" { + command = apply + + module { + source = "../../infrastructure" + } + + variables { + vault_name = run.setup_tests.vault_name + vault_location = "uksouth" + } + + assert { + condition = azurerm_resource_group.resource_group.name == "rg-nhsbackup-${var.vault_name}" + error_message = "Resource group name not as expected." + } + + assert { + condition = azurerm_resource_group.resource_group.location == var.vault_location + error_message = "Resource group location not as expected." + } +} \ No newline at end of file diff --git a/tests/integration-tests/setup/main.tf b/tests/integration-tests/setup/main.tf new file mode 100644 index 0000000..d85fb31 --- /dev/null +++ b/tests/integration-tests/setup/main.tf @@ -0,0 +1,16 @@ +terraform { + required_providers { + random = { + source = "hashicorp/random" + version = "3.5.1" + } + } +} + +resource "random_pet" "vault_name" { + length = 4 +} + +output "vault_name" { + value = random_pet.vault_name.id +}