Skip to content

Commit

Permalink
Azure monitor and log analytics integration (#22)
Browse files Browse the repository at this point in the history
* Added diagnostic settings resource and related configuration.

* Added end to end tests for diagnostic settings.

* Added docs for diagnostic settings and log analytics integration.

* Fixed compile error in e2e pgsql test.
  • Loading branch information
johncollinson2001 authored Oct 17, 2024
1 parent 493d7f4 commit 3e9dd57
Show file tree
Hide file tree
Showing 11 changed files with 334 additions and 13 deletions.
10 changes: 6 additions & 4 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@ The following is an example of how the module should be used:

```terraform
module "my_backup" {
source = "github.com/nhsdigital/az-backup//infrastructure"
vault_name = "myvault"
vault_location = "uksouth"
vault_redundancy = "LocallyRedundant"
source = "github.com/nhsdigital/az-backup//infrastructure"
vault_name = "myvault"
vault_location = "uksouth"
vault_redundancy = "LocallyRedundant"
log_analytics_workspace_id = azurerm_log_analytics_workspace.my_workspace.id
blob_storage_backups = {
backup1 = {
backup_name = "storage1"
Expand Down Expand Up @@ -92,6 +93,7 @@ To deploy the module an Azure identity (typically an app registration with clien
| `vault_name` | The name of the backup vault. The value supplied will be automatically prefixed with `rg-nhsbackup-`. If more than one az-backup module is created, this value must be unique across them. | Yes | n/a |
| `vault_location` | The location of the resource group that is created to contain the vault. | No | `uksouth` |
| `vault_redundancy` | The redundancy of the vault, e.g. `GeoRedundant`. [See the following link for the possible values](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/data_protection_backup_vault#redundancy) | No | `LocallyRedundant` |
| `log_analytics_workspace_id` | The id of the log analytics workspace that backup telemetry and diagnostics should be sent to. When no value is provided then diagnostics will not be sent anywhere. | No | n/a |
| `blob_storage_backups` | A map of blob storage backups that should be created. For each backup the following values should be provided: `storage_account_id`, `backup_name` and `retention_period`. When no value is provided then no backups are created. | No | n/a |
| `blob_storage_backups.storage_account_id` | The id of the storage account that should be backed up. | Yes | n/a |
| `blob_storage_backups.backup_name` | The name of the backup, which must be unique across blob storage backups. | Yes | n/a |
Expand Down
34 changes: 34 additions & 0 deletions infrastructure/backup_vault.tf
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,37 @@ resource "azurerm_data_protection_backup_vault" "backup_vault" {
type = "SystemAssigned"
}
}

locals {
backup_vault_diagnostics_log_categories = toset([
"AddonAzureBackupJobs",
"AddonAzureBackupPolicy",
"AddonAzureBackupProtectedInstance",
"CoreAzureBackup"
])

backup_vault_diagnostics_metric_categories = toset([
"Health"
])
}

resource "azurerm_monitor_diagnostic_setting" "backup_vault" {
count = length(var.log_analytics_workspace_id) > 0 ? 1 : 0
name = "bvault-${var.vault_name}-diagnostic-settings"
target_resource_id = azurerm_data_protection_backup_vault.backup_vault.id
log_analytics_workspace_id = var.log_analytics_workspace_id

dynamic "enabled_log" {
for_each = toset(local.backup_vault_diagnostics_log_categories)
content {
category = enabled_log.key
}
}

dynamic "metric" {
for_each = toset(local.backup_vault_diagnostics_metric_categories)
content {
category = metric.key
}
}
}
6 changes: 6 additions & 0 deletions infrastructure/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ variable "vault_redundancy" {
default = "LocallyRedundant"
}

variable "log_analytics_workspace_id" {
type = string
default = ""
}

variable "blob_storage_backups" {
type = map(object({
backup_name = string
Expand Down Expand Up @@ -45,3 +50,4 @@ variable "postgresql_flexible_server_backups" {
}))
default = {}
}

2 changes: 1 addition & 1 deletion tests/end-to-end-tests/blob_storage_backup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ type TestBlobStorageBackupExternalResources struct {
*/
func setupExternalResourcesForBlobStorageBackupTest(t *testing.T, credential *azidentity.ClientSecretCredential, subscriptionID string, vault_name string, vault_location string) *TestBlobStorageBackupExternalResources {
resourceGroupName := fmt.Sprintf("rg-nhsbackup-%s-external", vault_name)
resourceGroup := CreateResourceGroup(t, subscriptionID, credential, resourceGroupName, vault_location)
resourceGroup := CreateResourceGroup(t, credential, subscriptionID, resourceGroupName, vault_location)

storageAccountOneName := fmt.Sprintf("sa%sexternal1", strings.ToLower(vault_name))
storageAccountOne := CreateStorageAccount(t, credential, subscriptionID, resourceGroupName, storageAccountOneName, vault_location)
Expand Down
143 changes: 143 additions & 0 deletions tests/end-to-end-tests/diagnostic_settings_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package e2e_tests

import (
"fmt"
"strings"
"testing"

"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/operationalinsights/armoperationalinsights"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources"
"github.com/gruntwork-io/terratest/modules/random"
"github.com/gruntwork-io/terratest/modules/terraform"
test_structure "github.com/gruntwork-io/terratest/modules/test-structure"
"github.com/stretchr/testify/assert"
)

type TestDiagnosticSettingsExternalResources struct {
ResourceGroup armresources.ResourceGroup
LogAnalyticsWorkspace armoperationalinsights.Workspace
}

/*
* Creates resources which are "external" to the az-backup module, and models
* what would be backed up in a real scenario.
*/
func setupExternalResourcesForDiagnosticSettingsTest(t *testing.T, credential *azidentity.ClientSecretCredential, subscriptionID string, vault_name string, vault_location string) *TestDiagnosticSettingsExternalResources {
resourceGroupName := fmt.Sprintf("rg-nhsbackup-%s-external", vault_name)
resourceGroup := CreateResourceGroup(t, credential, subscriptionID, resourceGroupName, vault_location)

logAnalyticsWorkspaceName := fmt.Sprintf("law-%s-external", strings.ToLower(vault_name))
logAnalyticsWorkspace := CreateLogAnalyticsWorkspace(t, credential, subscriptionID, resourceGroupName, logAnalyticsWorkspaceName, vault_location)

externalResources := &TestDiagnosticSettingsExternalResources{
ResourceGroup: resourceGroup,
LogAnalyticsWorkspace: logAnalyticsWorkspace,
}

return externalResources
}

/*
* TestDiagnosticSettings tests the configuration of the backup vaults diagnostics settings and ensures they
* integrate with an external log analytics workspace.
*/
func TestDiagnosticSettings(t *testing.T) {
t.Parallel()

environment := GetEnvironmentConfiguration(t)
credential := GetAzureCredential(t, environment)

vaultName := random.UniqueId()
vaultLocation := "uksouth"
vaultRedundancy := "LocallyRedundant"
resourceGroupName := fmt.Sprintf("rg-nhsbackup-%s", vaultName)
backupVaultName := fmt.Sprintf("bvault-%s", vaultName)

externalResources := setupExternalResourcesForDiagnosticSettingsTest(t, credential, environment.SubscriptionID, vaultName, vaultLocation)

// Teardown stage
// ...

defer test_structure.RunTestStage(t, "teardown", func() {
terraformOptions := test_structure.LoadTerraformOptions(t, environment.TerraformFolder)

terraform.Destroy(t, terraformOptions)

DeleteResourceGroup(t, credential, environment.SubscriptionID, *externalResources.ResourceGroup.Name)
})

// Setup stage
// ...

test_structure.RunTestStage(t, "setup", func() {
terraformOptions := &terraform.Options{
TerraformDir: environment.TerraformFolder,

Vars: map[string]interface{}{
"vault_name": vaultName,
"vault_location": vaultLocation,
"vault_redundancy": vaultRedundancy,
"log_analytics_workspace_id": *externalResources.LogAnalyticsWorkspace.ID,
},

BackendConfig: map[string]interface{}{
"resource_group_name": environment.TerraformStateResourceGroup,
"storage_account_name": environment.TerraformStateStorageAccount,
"container_name": environment.TerraformStateContainer,
"key": vaultName + ".tfstate",
},
}

// Save options for later test stages
test_structure.SaveTerraformOptions(t, environment.TerraformFolder, terraformOptions)

terraform.InitAndApply(t, terraformOptions)
})

// Validate stage
// ...

test_structure.RunTestStage(t, "validate", func() {
// An array of log categories that we expect to be enabled for the diagnostic settings
expectedLogCategories := []string{
"AddonAzureBackupJobs",
"AddonAzureBackupPolicy",
"AddonAzureBackupProtectedInstance",
"CoreAzureBackup",
}

// An array of metrics that we expect to be enabled for the diagnostic settings
expectedMetricCategories := []string{
"Health",
}

backupVault := GetBackupVault(t, credential, environment.SubscriptionID, resourceGroupName, backupVaultName)
diagnosticSettings := GetDiagnosticSettings(t, credential, *backupVault.ID, *backupVault.Name)

assert.Equal(t, len(diagnosticSettings.Properties.Logs), len(expectedLogCategories), "Expected to find %2 log categories in diagnostic settings", len(expectedLogCategories))
assert.Equal(t, len(diagnosticSettings.Properties.Metrics), len(expectedMetricCategories), "Expected to find %2 metric categories in diagnostic settings", len(expectedMetricCategories))

for _, expectedCategory := range expectedLogCategories {
found := false
for _, log := range diagnosticSettings.Properties.Logs {
if *log.Category == expectedCategory {
found = true
break
}
}
assert.True(t, found, "Expected log category %s not found in diagnostic settings", expectedCategory)
}

for _, expectedCategory := range expectedMetricCategories {
found := false
for _, metric := range diagnosticSettings.Properties.Metrics {
if *metric.Category == expectedCategory {
found = true
break
}
}
assert.True(t, found, "Expected metric category %s not found in diagnostic settings", expectedCategory)
}
})
}
4 changes: 3 additions & 1 deletion tests/end-to-end-tests/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ require (
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization v1.0.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute v1.0.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dataprotection/armdataprotection v1.0.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/monitor/armmonitor v0.11.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/operationalinsights/armoperationalinsights v1.2.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresqlflexibleservers v1.1.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.0.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.1.1
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.0.0
github.com/gruntwork-io/terratest v0.47.1
github.com/stretchr/testify v1.9.0
Expand Down
16 changes: 12 additions & 4 deletions tests/end-to-end-tests/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -204,14 +204,22 @@ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute v1.0.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute v1.0.0/go.mod h1:gM3K25LQlsET3QR+4V74zxCsFAy0r6xMNN9n80SZn+4=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dataprotection/armdataprotection v1.0.0 h1:VFqjVi532z3gdltbAkYrPl9Ez0czn3ZPM+bjmvLq6fk=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dataprotection/armdataprotection v1.0.0/go.mod h1:CmZQSRwBPP7KNjDA+PHaoR2m8wgOsbTd9ncqZgSzgHA=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal v1.0.0 h1:lMW1lD/17LUA5z1XTURo7LcVG2ICBPlyMHjIUrcFZNQ=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal v1.0.0/go.mod h1:ceIuwmxDWptoW3eCqSXlnPsZFKh4X+R38dWPv7GS9Vs=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal v1.1.2 h1:mLY+pNLjCUeKhgnAJWAKhEUQM+RJQo2H1fuGSw1Ky1E=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal v1.1.2/go.mod h1:FbdwsQ2EzwvXxOPcMFYO8ogEc9uMMIj3YkmCdXdAFmk=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v2 v2.0.0 h1:PTFGRSlMKCQelWwxUyYVEUqseBJVemLyqWJjvMyt0do=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v2 v2.0.0/go.mod h1:LRr2FzBTQlONPPa5HREE5+RjSCTXl7BwOvYOaWTqCaI=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/managementgroups/armmanagementgroups v1.0.0 h1:pPvTJ1dY0sA35JOeFq6TsY2xj6Z85Yo23Pj4wCCvu4o=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/managementgroups/armmanagementgroups v1.0.0/go.mod h1:mLfWfj8v3jfWKsL9G4eoBoXVcsqcIUTapmdKy7uGOp0=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/monitor/armmonitor v0.11.0 h1:Ds0KRF8ggpEGg4Vo42oX1cIt/IfOhHWJBikksZbVxeg=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/monitor/armmonitor v0.11.0/go.mod h1:jj6P8ybImR+5topJ+eH6fgcemSFBmU6/6bFF8KkwuDI=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork v1.0.0 h1:nBy98uKOIfun5z6wx6jwWLrULcM0+cjBalBFZlEZ7CA=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork v1.0.0/go.mod h1:243D9iHbcQXoFUtgHJwL7gl2zx1aDuDMjvBZVGr2uW0=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresqlflexibleservers v1.1.0 h1:HzqcSJWx32XQdr8KtxAu/SZJj0PqDo9tKf2YGPdynV0=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresqlflexibleservers v1.1.0/go.mod h1:nKcJObAisSPDrO9lMuuCBoYY7Ki7ADt8p6XmBhpKNTk=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.0.0 h1:ECsQtyERDVz3NP3kvDOTLvbQhqWp/x9EsGKtb4ogUr8=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.0.0/go.mod h1:s1tW/At+xHqjNFvWU4G0c0Qv33KOhvbGNj0RCTQDV8s=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/operationalinsights/armoperationalinsights v1.2.0 h1:4FlNvfcPu7tTvOgOzXxIbZLvwvmZq1OdhQUdIa9g2N4=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/operationalinsights/armoperationalinsights v1.2.0/go.mod h1:A4nzEXwVd5pAyneR6KOvUAo72svUc5rmCzRHhAbP6lA=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.1.1 h1:7CBQ+Ei8SP2c6ydQTGCCrS35bDxgTMfoP2miAwK++OU=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.1.1/go.mod h1:c/wcGeGx5FUPbM/JltUYHZcKmigwyVLJlDq+4HdtXaw=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.0.0 h1:TMEyRFKh1zaSPmoQh3kxK+xRAYVq8guCI/7SMO0F3KY=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.0.0/go.mod h1:c+Lifp3EDEamAkPVzMooRNOK6CZjNSdEnf1A7jsI9u4=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.0.0 h1:TMEyRFKh1zaSPmoQh3kxK+xRAYVq8guCI/7SMO0F3KY=
Expand Down
61 changes: 60 additions & 1 deletion tests/end-to-end-tests/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import (
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dataprotection/armdataprotection"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/monitor/armmonitor"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/operationalinsights/armoperationalinsights"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresqlflexibleservers"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage"
Expand Down Expand Up @@ -168,6 +170,32 @@ func GetRoleAssignment(t *testing.T, credential *azidentity.ClientSecretCredenti
return nil
}

func GetDiagnosticSettings(t *testing.T, credential *azidentity.ClientSecretCredential, resourceID string, resourceName string) *armmonitor.DiagnosticSettingsResource {
client, err := armmonitor.NewDiagnosticSettingsClient(credential, nil)
assert.NoError(t, err, "Failed to create diagnostic settings client: %v", err)

// List the diagnostic settings for the given resource
pager := client.NewListPager(resourceID, nil)

for pager.More() {
page, err := pager.NextPage(context.Background())
assert.NoError(t, err, "Failed to list diagnostic settings")

// We currently only handle when there's only one diagnostic setting per resource
// ...

if len(page.Value) == 0 {
assert.Fail(t, "No diagnostic settings found for resource: %s", resourceName)
} else if len(page.Value) > 1 {
assert.Fail(t, "Multiple diagnostic settings found for resource: %s", resourceName)
} else {
return page.Value[0]
}
}

return nil
}

/*
* Gets a backup vault for the provided name.
*/
Expand Down Expand Up @@ -266,7 +294,8 @@ func GetBackupInstanceForName(instances []*armdataprotection.BackupInstanceResou
/*
* Creates a resource group that can be used for testing purposes.
*/
func CreateResourceGroup(t *testing.T, subscriptionID string, credential *azidentity.ClientSecretCredential, resourceGroupName string, resourceGroupLocation string) armresources.ResourceGroup {
func CreateResourceGroup(t *testing.T, credential *azidentity.ClientSecretCredential, subscriptionID string,
resourceGroupName string, resourceGroupLocation string) armresources.ResourceGroup {
client, err := armresources.NewResourceGroupsClient(subscriptionID, credential, nil)
assert.NoError(t, err, "Failed to create resource group client: %v", err)

Expand All @@ -287,6 +316,36 @@ func CreateResourceGroup(t *testing.T, subscriptionID string, credential *aziden
return resp.ResourceGroup
}

/*
* Creates a Log Analytics workspace that can be used for testing purposes.
*/
func CreateLogAnalyticsWorkspace(t *testing.T, credential *azidentity.ClientSecretCredential, subscriptionID string,
resourceGroupName string, workspaceName string, workspaceLocation string) armoperationalinsights.Workspace {
client, err := armoperationalinsights.NewWorkspacesClient(subscriptionID, credential, nil)
assert.NoError(t, err, "Failed to create Log Analytics workspace client: %v", err)

log.Printf("Creating log analytics workspace %s in location %s", workspaceName, workspaceLocation)

pollerResp, err := client.BeginCreateOrUpdate(
context.Background(),
resourceGroupName,
workspaceName,
armoperationalinsights.Workspace{
Location: &workspaceLocation,
},
nil,
)
assert.NoError(t, err, "Failed to begin creating log analytics workspace: %v", err)

// Wait for the creation to complete
resp, err := pollerResp.PollUntilDone(context.Background(), nil)
assert.NoError(t, err, "Failed to create log analytics workspace: %v", err)

log.Printf("Log analytics workspace %s created successfully", workspaceName)

return resp.Workspace
}

/*
* Creates a storage account that can be used for testing purposes.
*/
Expand Down
2 changes: 1 addition & 1 deletion tests/end-to-end-tests/managed_disk_backup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ type TestManagedDiskBackupExternalResources struct {
*/
func setupExternalResourcesForManagedDiskBackupTest(t *testing.T, credential *azidentity.ClientSecretCredential, subscriptionID string, vault_name string, vault_location string) *TestManagedDiskBackupExternalResources {
resourceGroupName := fmt.Sprintf("rg-nhsbackup-%s-external", vault_name)
resourceGroup := CreateResourceGroup(t, subscriptionID, credential, resourceGroupName, vault_location)
resourceGroup := CreateResourceGroup(t, credential, subscriptionID, resourceGroupName, vault_location)

managedDiskOneName := fmt.Sprintf("disk-%s-external-1", strings.ToLower(vault_name))
managedDiskOne := CreateManagedDisk(t, credential, subscriptionID, resourceGroupName, managedDiskOneName, vault_location, int32(1))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ type TestPostgresqlFlexibleServerBackupExternalResources struct {
*/
func setupExternalResourcesForPostgresqlFlexibleServerBackupTest(t *testing.T, credential *azidentity.ClientSecretCredential, subscriptionID string, vault_name string, vault_location string) *TestPostgresqlFlexibleServerBackupExternalResources {
resourceGroupName := fmt.Sprintf("rg-nhsbackup-%s-external", vault_name)
resourceGroup := CreateResourceGroup(t, subscriptionID, credential, resourceGroupName, vault_location)
resourceGroup := CreateResourceGroup(t, credential, subscriptionID, resourceGroupName, vault_location)

PostgresqlFlexibleServerOneName := fmt.Sprintf("pgflexserver-%s-external-1", strings.ToLower(vault_name))
PostgresqlFlexibleServerOne := CreatePostgresqlFlexibleServer(t, credential, subscriptionID, resourceGroupName, PostgresqlFlexibleServerOneName, vault_location, int32(32))
Expand Down
Loading

0 comments on commit 3e9dd57

Please sign in to comment.