diff --git a/.github/workflows/standalone-compute.json b/.github/workflows/standalone-compute.json index a1effb7e50..ebda14526e 100644 --- a/.github/workflows/standalone-compute.json +++ b/.github/workflows/standalone-compute.json @@ -50,6 +50,7 @@ "compute/virtual_machine/215-vm-keyvault-for-windows-extension", "compute/virtual_machine/216-vm-linux_diagnostic_extensions", "compute/virtual_machine/217-vm-disk-encryption-set-msi", + "compute/virtual_machine/300-single-windows-recovery-site", "compute/vmware_cluster/101-vmware_cluster" ] } diff --git a/.github/workflows/standalone-scenarios.json b/.github/workflows/standalone-scenarios.json index 36d5c48490..b3633d7818 100644 --- a/.github/workflows/standalone-scenarios.json +++ b/.github/workflows/standalone-scenarios.json @@ -93,6 +93,9 @@ "recovery_vault/105-asr-with-network-mapping", "recovery_vault/106-backupvault-with-sqldatabase-saphana", "recovery_vault/107-asr-diagnostics", + "recovery_vault/108-simple-asr-plan", + "recovery_vault/109-asr-with-cmk-and-msi", + "recovery_vault/110-asr-with-custom-encryption-key", "redis_cache/100-redis-standard", "redis_cache/101-redis-diagnostics", "redis_cache/102-redis-private", diff --git a/examples/cognitive_services/101-cognitive-services-account-managed-identity/configuration.tfvars b/examples/cognitive_services/101-cognitive-services-account-managed-identity/configuration.tfvars index 79f8ab921a..62c98ec3e1 100644 --- a/examples/cognitive_services/101-cognitive-services-account-managed-identity/configuration.tfvars +++ b/examples/cognitive_services/101-cognitive-services-account-managed-identity/configuration.tfvars @@ -27,14 +27,14 @@ cognitive_services_account = { # lz_key = "examples" key = "test-rg" } - name = "cs-test-1" - kind = "OpenAI" - sku_name = "S0" + name = "cs-test-1" + kind = "OpenAI" + sku_name = "S0" public_network_access_enabled = true identity = { type = "SystemAssigned, UserAssigned" // Can be "SystemAssigned, UserAssigned" or "SystemAssigned" or "UserAssigned" - key = "cognitive_msi" // A must with "SystemAssigned, UserAssigned" and "UserAssigned" + key = "cognitive_msi" // A must with "SystemAssigned, UserAssigned" and "UserAssigned" } tags = { diff --git a/examples/compute/virtual_machine/300-single-windows-recovery-site/configuration.tfvars b/examples/compute/virtual_machine/300-single-windows-recovery-site/configuration.tfvars new file mode 100644 index 0000000000..76e8d47139 --- /dev/null +++ b/examples/compute/virtual_machine/300-single-windows-recovery-site/configuration.tfvars @@ -0,0 +1,246 @@ +global_settings = { + regions = { + region1 = "francecentral" + } +} + + +storage_accounts = { + recovery = { + name = "storage1-recovery" + resource_group_key = "backup" + account_kind = "Storage" + account_tier = "Standard" + account_replication_type = "LRS" + min_tls_version = "TLS1_2" + public_network_access_enabled = true + } +} + +recovery_vaults = { + asr1 = { + name = "vault_re1" + resource_group_key = "backup" + region = "region1" + soft_delete_enabled = true + public_network_access_enabled = false + + replication_policies = { + repl1 = { + name = "policy1" + recovery_point_retention_in_minutes = 24 * 60 + application_consistent_snapshot_frequency_in_minutes = 0 + } + } + + identity = { + # Do not use "SystemAssigned, UserAssigned", there is a bug in the provider + type = "UserAssigned" + managed_identities = { + asr1 = { + key = "asr" + } + } + } + + recovery_fabrics = { + fabric1 = { + name = "fab_re1" + resource_group_key = "backup" + region = "region1" + } + } + + protection_containers = { + container1 = { + name = "prtc1" + resource_group_key = "iaas" + recovery_fabric_key = "fabric1" + } + container2 = { + name = "prtc2" + resource_group_key = "iaas-secondary" + recovery_fabric_key = "fabric1" + } + } + } +} + +managed_identities = { + asr = { + name = "asr" + resource_group_key = "backup" + } +} + +virtual_machines = { + app01 = { + resource_group_key = "iaas" + provision_vm_agent = true + # boot_diagnostics_storage_account_key = "bootdiag_region1" + os_type = "windows" + keyvault_key = "app" + + networking_interfaces = { + nic0 = { + vnet_key = "vnet1" + subnet_key = "iaas" + name = "nic0" + enable_ip_forwarding = false + internal_dns_name_label = "example_vm1" + } + } + + replication = { + vault_key = "asr1" + policy_key = "repl1" + staging_storage_account_key = "recovery" + + source = { + recovery_fabric_key = "fabric1" + protection_container_key = "container1" + } + + target = { + recovery_fabric_key = "fabric1" + protection_container_key = "container2" + zone = "2" + resource_group_key = "iaas-secondary" + os_disk_storage_type = "Standard_LRS" + data_disk_storage_type = "Standard_LRS" + network = { + vnet_key = "vnet1" + subnet_key = "iaas_secondary" + } + test_network = { + vnet_key = "vnet2" + subnet_key = "test_iaas_secondary" + } + } + } + + virtual_machine_settings = { + windows = { + name = "example_vm1" + size = "Standard_B2s_v2" + admin_username = "adminuser" + license_type = "Windows_Server" + network_interface_keys = ["nic0"] + zone = "1" + os_disk = { + name = "example_vm1-os" + caching = "ReadWrite" + storage_account_type = "StandardSSD_LRS" + # disk_encryption_set_key = "set1" + zone = "1" + } + source_image_reference = { + publisher = "MicrosoftWindowsServer" + offer = "WindowsServer" + sku = "2019-datacenter-gensecond" + version = "latest" + } + } + } + } +} + +vnets = { + vnet1 = { + resource_group_key = "networking" + region = "region1" + vnet = { + name = "vnet1" + address_space = ["10.226.0.0/19"] + } + + specialsubnets = {} + subnets = { + iaas = { + name = "NET-IAAS" + cidr = ["10.226.18.0/23"] + service_endpoints = ["Microsoft.KeyVault", "Microsoft.Storage"] + } + iaas_secondary = { + name = "NET-IAAS-SECONDARY" + cidr = ["10.226.22.0/23"] + service_endpoints = ["Microsoft.KeyVault", "Microsoft.Storage"] + } + } + } + vnet2 = { + resource_group_key = "networking" + region = "region1" + vnet = { + name = "vnet2" + address_space = ["10.224.136.0/23"] + } + + specialsubnets = {} + subnets = { + test_iaas_secondary = { + name = "NET-IAAS-SECONDARY" + cidr = ["10.224.136.0/23"] + service_endpoints = ["Microsoft.KeyVault", "Microsoft.Storage"] + } + } + } +} + +keyvaults = { + app = { + name = "kv1-recovery" + resource_group_key = "iaas" + sku_name = "standard" + soft_delete_enabled = true + public_network_access_enabled = true + } +} + +resource_groups = { + networking = { + name = "network_re1" + } + + iaas = { + name = "sharedsvc_re1" + } + + iaas-secondary = { + name = "sharedsvc_re2" + } + + backup = { + name = "backup_re1" + } +} + +role_mapping = { + built_in_role_mapping = { + resource_groups = { + backup = { + "Contributor" = { + managed_identities = { + keys = [ + "asr", + ] + } + } + "Storage Blob Data Contributor" = { + managed_identities = { + keys = [ + "asr", + ] + } + } + "Storage Blob Data Owner" = { + managed_identities = { + keys = [ + "asr", + ] + } + } + } + } + } +} diff --git a/examples/recovery_vault/108-simple-asr-plan/configuration.tfvars b/examples/recovery_vault/108-simple-asr-plan/configuration.tfvars new file mode 100644 index 0000000000..ae388965c4 --- /dev/null +++ b/examples/recovery_vault/108-simple-asr-plan/configuration.tfvars @@ -0,0 +1,245 @@ +global_settings = { + regions = { + region1 = "francecentral" + } +} + + +storage_accounts = { + recovery = { + name = "storage1-recovery" + resource_group_key = "backup" + account_kind = "Storage" + account_tier = "Standard" + account_replication_type = "LRS" + min_tls_version = "TLS1_2" + public_network_access_enabled = true + } +} + +recovery_vaults = { + asr1 = { + name = "vault_re1" + resource_group_key = "backup" + region = "region1" + soft_delete_enabled = true + public_network_access_enabled = false + + replication_policies = { + repl1 = { + name = "policy1" + recovery_point_retention_in_minutes = 24 * 60 + application_consistent_snapshot_frequency_in_minutes = 0 + } + } + + identity = { + # Do not use "SystemAssigned, UserAssigned", there is a bug in the provider + type = "UserAssigned" + managed_identities = { + asr1 = { + key = "asr" + } + } + } + + recovery_fabrics = { + fabric1 = { + name = "fab_re1" + resource_group_key = "backup" + region = "region1" + } + } + + protection_containers = { + container1 = { + name = "prtc1" + resource_group_key = "iaas" + recovery_fabric_key = "fabric1" + } + container2 = { + name = "prtc2" + resource_group_key = "iaas-secondary" + recovery_fabric_key = "fabric1" + } + } + } +} + +managed_identities = { + asr = { + name = "asr" + resource_group_key = "backup" + } +} + +virtual_machines = { + app01 = { + resource_group_key = "iaas" + provision_vm_agent = true + # boot_diagnostics_storage_account_key = "bootdiag_region1" + os_type = "windows" + keyvault_key = "app" + + networking_interfaces = { + nic0 = { + vnet_key = "vnet1" + subnet_key = "iaas" + name = "nic0" + enable_ip_forwarding = false + internal_dns_name_label = "example_vm1" + } + } + + replication = { + vault_key = "asr1" + policy_key = "repl1" + staging_storage_account_key = "recovery" + + source = { + recovery_fabric_key = "fabric1" + protection_container_key = "container1" + } + + target = { + recovery_fabric_key = "fabric1" + protection_container_key = "container2" + zone = "2" + resource_group_key = "iaas-secondary" + network = { + vnet_key = "vnet1" + subnet_key = "iaas_secondary" + } + test_network = { + vnet_key = "vnet2" + subnet_key = "test_iaas_secondary" + } + } + } + + virtual_machine_settings = { + windows = { + name = "example_vm1" + size = "Standard_B2s_v2" + admin_username = "adminuser" + license_type = "Windows_Server" + network_interface_keys = ["nic0"] + zone = "1" + os_disk = { + name = "example_vm1-os" + caching = "ReadWrite" + storage_account_type = "StandardSSD_LRS" + # disk_encryption_set_key = "set1" + zone = "1" + } + source_image_reference = { + publisher = "MicrosoftWindowsServer" + offer = "WindowsServer" + sku = "2019-datacenter-gensecond" + version = "latest" + } + } + } + } +} + +vnets = { + vnet1 = { + resource_group_key = "networking" + region = "region1" + vnet = { + name = "vnet1" + address_space = ["10.226.0.0/19"] + } + + specialsubnets = {} + subnets = { + iaas = { + name = "NET-IAAS" + cidr = ["10.226.18.0/23"] + service_endpoints = ["Microsoft.KeyVault", "Microsoft.Storage"] + } + iaas_secondary = { + name = "NET-IAAS-SECONDARY" + cidr = ["10.226.22.0/23"] + service_endpoints = ["Microsoft.KeyVault", "Microsoft.Storage"] + } + } + } + vnet2 = { + resource_group_key = "networking" + region = "region1" + vnet = { + name = "vnet2" + address_space = ["10.224.136.0/23"] + } + + specialsubnets = {} + subnets = { + test_iaas_secondary = { + name = "NET-IAAS-SECONDARY" + cidr = ["10.224.136.0/23"] + service_endpoints = ["Microsoft.KeyVault", "Microsoft.Storage"] + } + } + } +} + +keyvaults = { + app = { + name = "kv1-recovery" + resource_group_key = "iaas" + sku_name = "standard" + soft_delete_enabled = true + public_network_access_enabled = true + } +} + +resource_groups = { + networking = { + name = "network_re1" + } + + iaas = { + name = "sharedsvc_re1" + } + + iaas-secondary = { + name = "sharedsvc_re2" + } + + backup = { + name = "backup_re1" + } +} + +role_mapping = { + built_in_role_mapping = { + resource_groups = { + backup = { + "Contributor" = { + managed_identities = { + keys = [ + "asr", + ] + } + } + "Storage Blob Data Contributor" = { + managed_identities = { + keys = [ + "asr", + ] + } + } + "Storage Blob Data Owner" = { + managed_identities = { + keys = [ + "asr", + ] + } + } + } + } + } +} + diff --git a/examples/recovery_vault/108-simple-asr-plan/recovery_plans.tfvars b/examples/recovery_vault/108-simple-asr-plan/recovery_plans.tfvars new file mode 100644 index 0000000000..1dd8817632 --- /dev/null +++ b/examples/recovery_vault/108-simple-asr-plan/recovery_plans.tfvars @@ -0,0 +1,32 @@ +recovery_plans = { + plan1 = { + name = "asrplan1" + recovery_vault_key = "asr1" + source_recovery_fabric_key = "fabric1" + target_recovery_fabric_key = "fabric1" + azure_to_azure_settings = { + primary_zone = "1" + recovery_zone = "2" + } + boot_recovery_group = { + virtual_machines_key = ["app01"] + # replicated_protected_items_id = ["/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/sharedsvc_re1/providers/Microsoft.RecoveryServices/vaults/vault_re1/replicationFabrics/fab_re1/replicationProtectionContainers/prtc1/replicationProtectedItems/example_vm1-repl"] + } + shutdown_recovery_group = { + pre_action = { + name = "testshutdownPreAction" + type = "ManualActionDetails" + fail_over_directions = ["PrimaryToRecovery"] + fail_over_types = ["TestFailover"] + manual_action_instruction = "test shutdown instruction" + } + post_action = { + name = "testshutdownPostAction" + type = "ManualActionDetails" + fail_over_directions = ["PrimaryToRecovery"] + fail_over_types = ["TestFailover"] + manual_action_instruction = "test shutdown instruction" + } + } + } +} diff --git a/examples/recovery_vault/109-asr-with-cmk-and-msi/configuration.tfvars b/examples/recovery_vault/109-asr-with-cmk-and-msi/configuration.tfvars new file mode 100644 index 0000000000..b960abb714 --- /dev/null +++ b/examples/recovery_vault/109-asr-with-cmk-and-msi/configuration.tfvars @@ -0,0 +1,124 @@ +global_settings = { + regions = { + region1 = "australiaeast" + region2 = "australiacentral" + } +} + +resource_groups = { + primary = { + name = "sharedsvc_re1" + } +} + +# msi +managed_identities = { + asr1 = { + name = "asr1" + resource_group_key = "primary" + } +} + +recovery_vaults = { + asr1 = { + name = "vault_re1" + resource_group_key = "primary" + + region = "region1" + + replication_policies = { + repl1 = { + name = "policy1" + resource_group_key = "primary" + + recovery_point_retention_in_minutes = 24 * 60 + application_consistent_snapshot_frequency_in_minutes = 4 * 60 + } + } + + backup_policies = { + vms = { + policy1 = { + name = "VMBackupPolicy1" + vault_key = "asr1" + rg_key = "primary" + timezone = "UTC" + instant_restore_retention_days = 5 + backup = { + frequency = "Daily" + time = "23:00" + #if not desired daily, can pick weekdays as below: + #weekdays = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"] + } + retention_daily = { + count = 10 + } + retention_weekly = { + count = 42 + weekdays = ["Sunday", "Wednesday", "Friday", "Saturday"] + } + retention_monthly = { + count = 7 + weekdays = ["Sunday", "Wednesday"] + weeks = ["First", "Last"] + } + retention_yearly = { + count = 7 + weekdays = ["Sunday"] + weeks = ["Last"] + months = ["January"] + } + } + } + + fs = { + policy1 = { + name = "FSBackupPolicy1" + vault_key = "asr1" + rg_key = "primary" + timezone = "UTC" + backup = { + frequency = "Daily" + time = "23:00" + } + retention_daily = { + count = 1 + } + retention_weekly = { + count = 1 + weekdays = ["Sunday", "Wednesday", "Friday", "Saturday"] + } + retention_monthly = { + count = 1 + weekdays = ["Sunday", "Wednesday"] + weeks = ["First", "Last"] + } + retention_yearly = { + count = 2 + weekdays = ["Sunday"] + weeks = ["Last"] + months = ["January"] + } + } + } + } + + encryption = { + key_id = "https://myvault001.vault.azure.net/keys/asr-cmk/" + infrastructure_encryption_enabled = true + user_assigned_identity = { + key = "asr1" + } + } + + identity = { + # Do not use "SystemAssigned, UserAssigned", there is a bug in the provider + type = "UserAssigned" + managed_identities = { + asr1 = { + key = "asr1" + } + } + } + } +} diff --git a/examples/recovery_vault/110-asr-with-custom-encryption-key/configuration.tfvars b/examples/recovery_vault/110-asr-with-custom-encryption-key/configuration.tfvars new file mode 100644 index 0000000000..3e2f4dc1d5 --- /dev/null +++ b/examples/recovery_vault/110-asr-with-custom-encryption-key/configuration.tfvars @@ -0,0 +1,103 @@ +global_settings = { + regions = { + region1 = "australiaeast" + region2 = "australiacentral" + } +} + +resource_groups = { + primary = { + name = "sharedsvc_re1" + } +} + +recovery_vaults = { + asr1 = { + name = "vault_re1" + resource_group_key = "primary" + + region = "region1" + + replication_policies = { + repl1 = { + name = "policy1" + resource_group_key = "primary" + + recovery_point_retention_in_minutes = 24 * 60 + application_consistent_snapshot_frequency_in_minutes = 4 * 60 + } + } + + backup_policies = { + vms = { + policy1 = { + name = "VMBackupPolicy1" + vault_key = "asr1" + rg_key = "primary" + timezone = "UTC" + instant_restore_retention_days = 5 + backup = { + frequency = "Daily" + time = "23:00" + #if not desired daily, can pick weekdays as below: + #weekdays = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"] + } + retention_daily = { + count = 10 + } + retention_weekly = { + count = 42 + weekdays = ["Sunday", "Wednesday", "Friday", "Saturday"] + } + retention_monthly = { + count = 7 + weekdays = ["Sunday", "Wednesday"] + weeks = ["First", "Last"] + } + retention_yearly = { + count = 7 + weekdays = ["Sunday"] + weeks = ["Last"] + months = ["January"] + } + } + } + + fs = { + policy1 = { + name = "FSBackupPolicy1" + vault_key = "asr1" + rg_key = "primary" + timezone = "UTC" + backup = { + frequency = "Daily" + time = "23:00" + } + retention_daily = { + count = 1 + } + retention_weekly = { + count = 1 + weekdays = ["Sunday", "Wednesday", "Friday", "Saturday"] + } + retention_monthly = { + count = 1 + weekdays = ["Sunday", "Wednesday"] + weeks = ["First", "Last"] + } + retention_yearly = { + count = 2 + weekdays = ["Sunday"] + weeks = ["Last"] + months = ["January"] + } + } + } + } + + encryption = { + key_id = "https://myvault001.vault.azure.net/keys/asr-cmk/" + infrastructure_encryption_enabled = true + } + } +} diff --git a/local.remote_objects.tf b/local.remote_objects.tf index 76de44f2cd..e723358ad0 100644 --- a/local.remote_objects.tf +++ b/local.remote_objects.tf @@ -131,6 +131,7 @@ locals { virtual_hubs = try(local.combined_objects_virtual_hubs, null) virtual_machine_scale_sets = try(local.combined_objects_virtual_machine_scale_sets, null) virtual_machines = try(local.combined_objects_virtual_machines, null) + virtual_machines_replication = try(local.combined_objects_virtual_machines_replication, null) virtual_subnets = try(local.combined_objects_virtual_subnets, null) virtual_wans = try(local.combined_objects_virtual_wans, null) vmware_clusters = try(local.combined_objects_vmware_clusters, null) diff --git a/locals.combined_objects.tf b/locals.combined_objects.tf index 8505d87082..50ef97c691 100644 --- a/locals.combined_objects.tf +++ b/locals.combined_objects.tf @@ -169,6 +169,7 @@ locals { combined_objects_virtual_hubs = merge(tomap({ (local.client_config.landingzone_key) = merge(module.virtual_hubs, try(var.data_sources.virtual_hubs, {})) }), try(var.remote_objects.virtual_hubs, {}), try(var.data_sources.virtual_hubs, {})) combined_objects_virtual_machine_scale_sets = merge(tomap({ (local.client_config.landingzone_key) = module.virtual_machine_scale_sets }), try(var.remote_objects.virtual_machine_scale_sets, {}), try(var.data_sources.virtual_machine_scale_sets, {})) combined_objects_virtual_machines = merge(tomap({ (local.client_config.landingzone_key) = module.virtual_machines }), try(var.remote_objects.virtual_machines, {}), try(var.data_sources.virtual_machines, {})) + combined_objects_virtual_machines_replication = merge(tomap({ (local.client_config.landingzone_key) = module.vm_replication }), try(var.remote_objects.virtual_machines_replication, {}), try(var.data_sources.virtual_machines_replication, {})) combined_objects_virtual_subnets = merge(tomap({ (local.client_config.landingzone_key) = merge(module.virtual_subnets, try(var.data_sources.virtual_subnets, {})) }), try(var.remote_objects.virtual_subnets, {})) combined_objects_vmware_clusters = merge(tomap({ (local.client_config.landingzone_key) = module.vmware_clusters }), try(var.remote_objects.vmware_clusters, {})) combined_objects_vmware_express_route_authorizations = merge(tomap({ (local.client_config.landingzone_key) = module.vmware_express_route_authorizations }), try(var.remote_objects.vmware_express_route_authorizations, {})) diff --git a/locals.tf b/locals.tf index f6038e943f..aa0582a766 100644 --- a/locals.tf +++ b/locals.tf @@ -102,6 +102,7 @@ locals { wvd_host_pools = try(var.compute.wvd_host_pools, {}) wvd_workspaces = try(var.compute.wvd_workspaces, {}) virtual_machines = try(var.compute.virtual_machines, {}) + virtual_machines_replication = try(var.compute.virtual_machines_replication, {}) virtual_machine_scale_sets = try(var.compute.virtual_machine_scale_sets, {}) runbooks = try(var.compute.runbooks, {}) } diff --git a/modules/compute/virtual_machine_replication/main.tf b/modules/compute/virtual_machine_replication/main.tf new file mode 100644 index 0000000000..16a670e1e8 --- /dev/null +++ b/modules/compute/virtual_machine_replication/main.tf @@ -0,0 +1,8 @@ +terraform { + required_providers { + azurecaf = { + source = "aztfmod/azurecaf" + } + } + +} \ No newline at end of file diff --git a/modules/compute/virtual_machine_replication/module.tf b/modules/compute/virtual_machine_replication/module.tf new file mode 100644 index 0000000000..620a0c46b2 --- /dev/null +++ b/modules/compute/virtual_machine_replication/module.tf @@ -0,0 +1,110 @@ +resource "azurerm_site_recovery_replicated_vm" "replication" { + count = try(var.settings.replication, null) == null ? 0 : 1 + + name = "${var.virtual_machine_name}-repl" + resource_group_name = coalesce( + try(var.settings.replication.recovery_vault_rg, null), + try(split("/", var.settings.replication.recovery_vault_id)[4], null), + try(var.recovery_vaults[var.client_config.landingzone_key][var.settings.replication.vault_key].resource_group_name, null), + try(var.recovery_vaults[var.settings.replication.lz_key][var.settings.replication.vault_key].resource_group_name, null) + ) + + recovery_vault_name = coalesce( + try(var.settings.replication.recovery_vault_name, null), + try(split("/", var.settings.replication.recovery_vault_id)[8], null), + try(var.recovery_vaults[var.client_config.landingzone_key][var.settings.replication.vault_key].name, null), + try(var.recovery_vaults[var.settings.replication.lz_key][var.settings.replication.vault_key].name, null) + ) + recovery_replication_policy_id = coalesce( + try(var.settings.replication.replication_policy_id, null), + try(var.recovery_vaults[var.client_config.landingzone_key][var.settings.replication.vault_key].replication_policies[var.settings.replication.policy_key].id, null), + try(var.recovery_vaults[var.settings.replication.lz_key][var.settings.replication.vault_key].replication_policies[var.settings.replication.policy_key].id, null) + ) + + source_recovery_fabric_name = coalesce( + try(var.settings.replication.source.recovery_fabric_name, null), + try(var.recovery_vaults[var.client_config.landingzone_key][var.settings.replication.vault_key].recovery_fabrics[var.settings.replication.source.recovery_fabric_key].name, null), + try(var.recovery_vaults[var.settings.replication.lz_key][var.settings.replication.vault_key].recovery_fabrics[var.settings.replication.source.recovery_fabric_key].name, null) + ) + source_vm_id = replace(replace(replace(lower(var.virtual_machine_id), "microsoft.compute", "Microsoft.Compute"), "resourcegroups", "resourceGroups"), "virtualmachines", "virtualMachines") + + source_recovery_protection_container_name = coalesce( + try(var.settings.replication.source.protection_container_name, null), + try(var.recovery_vaults[var.client_config.landingzone_key][var.settings.replication.vault_key].protection_containers[var.settings.replication.source.protection_container_key].name, null), + try(var.recovery_vaults[var.settings.replication.lz_key][var.settings.replication.vault_key].protection_containers[var.settings.replication.source.protection_container_key].name, null) + ) + + target_resource_group_id = coalesce( + try(var.resource_groups[var.client_config.landingzone_key][var.settings.replication.target.resource_group_key].id, null), + try(var.recovery_vaults[var.settings.replication.target.resource_group.lz_key][var.settings.replication.resource_group.key].id, null) + ) + target_recovery_fabric_id = coalesce( + try(var.settings.replication.target.recovery_fabric_id, null), + try(var.recovery_vaults[var.client_config.landingzone_key][var.settings.replication.vault_key].recovery_fabrics[var.settings.replication.target.recovery_fabric_key].id, null), + try(var.recovery_vaults[var.settings.replication.lz_key][var.settings.replication.vault_key].recovery_fabrics[var.settings.replication.target.recovery_fabric_key].id, null) + ) + target_recovery_protection_container_id = coalesce( + try(var.settings.replication.source.protection_container_id, null), + try(var.recovery_vaults[var.client_config.landingzone_key][var.settings.replication.vault_key].protection_containers[var.settings.replication.target.protection_container_key].id, null), + try(var.recovery_vaults[var.settings.replication.lz_key][var.settings.replication.vault_key].protection_containers[var.settings.replication.target.protection_container_key].id, null) + ) + target_zone = try(var.settings.replication.target.zone, "") + target_network_id = coalesce( + try(var.settings.replication.target.network.vnet_id, null), + try(var.vnets[try(var.settings.replication.target.network.lz_key, var.client_config.landingzone_key)][var.settings.replication.target.network.vnet_key].id, null) + ) + + test_network_id = coalesce( + try(var.settings.replication.target.test_network.vnet_id, null), + try(var.vnets[try(var.settings.replication.target.test_network.lz_key, var.client_config.landingzone_key)][var.settings.replication.target.test_network.vnet_key].id, null) + ) + + managed_disk { + disk_id = format("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Compute/disks/%s", var.client_config.subscription_id, lower(var.virtual_machine_os_disk.resource_group_name), lower(var.virtual_machine_os_disk.name)) + + staging_storage_account_id = coalesce( + try(var.storage_accounts[var.client_config.landingzone_key][var.settings.replication.staging_storage_account_key].id, null), + try(var.storage_accounts[var.settings.replication.staging_storage_account.lz_key][var.settings.replication.staging_storage_account.key].id, null) + ) + target_resource_group_id = coalesce( + try(var.resource_groups[var.client_config.landingzone_key][var.settings.replication.target.resource_group_key].id, null), + try(var.recovery_vaults[var.settings.replication.target.resource_group.lz_key][var.settings.replication.resource_group.key].id, null) + ) + target_disk_type = try(var.settings.replication.target.os_disk_storage_type, var.virtual_machine_os_disk.storage_account_type) # When I retrieve the storage account type, the plan detects a change and rebuilds the replication item. If anyone has an idea how to solve this problem + target_replica_disk_type = try(var.settings.replication.target.os_disk_replica_storage_type, var.settings.replication.target.os_disk_storage_type, var.virtual_machine_os_disk.storage_account_type) + target_disk_encryption_set_id = try(var.settings.os_disk.disk_encryption_set_key, null) == null ? null : var.disk_encryption_sets[try(var.settings.os_disk.lz_key, var.client_config.landingzone_key)][var.settings.os_disk.disk_encryption_set_key].id + } + + dynamic "managed_disk" { + for_each = lookup(var.settings, "data_disks", {}) + content { + + disk_id = replace(replace(lower(var.virtual_machine_data_disks[managed_disk.key]), "microsoft.compute", "Microsoft.Compute"), "resourcegroups", "resourceGroups") + + staging_storage_account_id = coalesce( + try(var.storage_accounts[var.client_config.landingzone_key][var.settings.replication.staging_storage_account_key].id, null), + try(var.storage_accounts[var.settings.replication.staging_storage_account.lz_key][var.settings.replication.staging_storage_account.key].id, null) + ) + target_resource_group_id = coalesce( + try(var.resource_groups[var.client_config.landingzone_key][var.settings.replication.target.resource_group_key].id, null), + try(var.recovery_vaults[var.settings.replication.target.resource_group.lz_key][var.settings.replication.resource_group.key].id, null) + ) + target_disk_type = try(var.settings.replication.target.data_disk_storage_type, managed_disk.value.storage_account_type) + target_replica_disk_type = try(var.settings.replication.target.data_disk_replica_storage_type, var.settings.replication.target.data_disk_storage_type, managed_disk.value.storage_account_type) + target_disk_encryption_set_id = try(managed_disk.value.disk_encryption_set_key, null) == null ? null : var.disk_encryption_sets[try(managed_disk.value.lz_key, var.client_config.landingzone_key)][managed_disk.value.disk_encryption_set_key].id + } + } + + dynamic "network_interface" { + for_each = var.virtual_machine_nics + content { + source_network_interface_id = network_interface.value.id + target_subnet_name = can(var.settings.replication.target.network.subnet_name) ? var.settings.replication.target.network.subnet_name : var.vnets[try(var.settings.replication.target.network.lz_key, var.client_config.landingzone_key)][var.settings.replication.target.network.vnet_key].subnets[var.settings.replication.target.network.subnet_key].name + failover_test_subnet_name = can(var.settings.replication.target.test_network.subnet_name) ? var.settings.replication.target.test_network.subnet_name : var.vnets[try(var.settings.replication.target.test_network.lz_key, var.client_config.landingzone_key)][var.settings.replication.target.test_network.vnet_key].subnets[var.settings.replication.target.test_network.subnet_key].name + } + } + + timeouts { + create = "24h" + } +} diff --git a/modules/compute/virtual_machine_replication/output.tf b/modules/compute/virtual_machine_replication/output.tf new file mode 100644 index 0000000000..5385091f1a --- /dev/null +++ b/modules/compute/virtual_machine_replication/output.tf @@ -0,0 +1,4 @@ +output "replicated_object_id" { + description = "The IDs of the Site Recovery Replicated VM." + value = azurerm_site_recovery_replicated_vm.replication[0].id +} diff --git a/modules/compute/virtual_machine_replication/variables.tf b/modules/compute/virtual_machine_replication/variables.tf new file mode 100644 index 0000000000..4973c40d0a --- /dev/null +++ b/modules/compute/virtual_machine_replication/variables.tf @@ -0,0 +1,45 @@ +variable "virtual_machine_id" { + type = string + nullable = false +} + +variable "virtual_machine_name" { + type = string + nullable = false +} + +variable "recovery_vaults" { + default = {} +} + +variable "resource_groups" { + default = {} +} + +variable "storage_accounts" { + default = {} +} + +variable "disk_encryption_sets" { + default = {} +} + +variable "settings" {} + +variable "vnets" {} + +variable "client_config" { + description = "Client configuration object (see module README.md)." +} + +variable "virtual_machine_data_disks" { + default = {} +} + +variable "virtual_machine_os_disk" { + default = {} +} + +variable "virtual_machine_nics" { + default = {} +} diff --git a/modules/recovery_vault/diagnostics.tf b/modules/recovery_vault/diagnostics.tf index 50f56b34e9..0a44065a6d 100644 --- a/modules/recovery_vault/diagnostics.tf +++ b/modules/recovery_vault/diagnostics.tf @@ -7,4 +7,4 @@ module "diagnostics" { resource_location = local.location diagnostics = var.diagnostics profiles = try(var.settings.diagnostic_profiles, {}) -} \ No newline at end of file +} diff --git a/modules/recovery_vault/outputs.tf b/modules/recovery_vault/outputs.tf index 4213ffee07..da18197c57 100644 --- a/modules/recovery_vault/outputs.tf +++ b/modules/recovery_vault/outputs.tf @@ -39,3 +39,13 @@ output "rbac_id" { description = "Principal Id of the Vault" value = try(azurerm_recovery_services_vault.asr.identity.0.principal_id, null) } + +output "recovery_fabrics" { + description = "Output the set of replication fabrics in the vault" + value = azurerm_site_recovery_fabric.recovery_fabric +} + +output "protection_containers" { + description = "Output the set of protection containers in the vault" + value = azurerm_site_recovery_protection_container.protection_container +} diff --git a/modules/recovery_vault/recovery_plan/azure_recovery_plan.tf b/modules/recovery_vault/recovery_plan/azure_recovery_plan.tf new file mode 100644 index 0000000000..15461456da --- /dev/null +++ b/modules/recovery_vault/recovery_plan/azure_recovery_plan.tf @@ -0,0 +1,123 @@ +resource "azurerm_site_recovery_replication_recovery_plan" "replication_plan" { + + name = var.settings.name + recovery_vault_id = var.recovery_vault_id + source_recovery_fabric_id = var.recovery_fabrics[var.settings.source_recovery_fabric_key].id + target_recovery_fabric_id = var.recovery_fabrics[var.settings.target_recovery_fabric_key].id + + + dynamic "shutdown_recovery_group" { + for_each = try(var.settings.shutdown_recovery_group, null) != null ? [var.settings.shutdown_recovery_group] : [] + content { + dynamic "pre_action" { + for_each = can(var.settings.shutdown_recovery_group.pre_action) ? [var.settings.shutdown_recovery_group.pre_action] : [] + content { + name = pre_action.value.name + type = pre_action.value.type # (Required) Type of the action detail. Possible values are AutomationRunbookActionDetails, ManualActionDetails and ScriptActionDetails. + fail_over_directions = pre_action.value.fail_over_directions # (Required) Directions of fail over. Possible values are PrimaryToRecovery and RecoveryToPrimary + fail_over_types = pre_action.value.fail_over_types # (Required) Types of fail over. Possible values are TestFailover, PlannedFailover and UnplannedFailover + fabric_location = try(pre_action.value.fabric_location, null) # (Optional) The fabric location of runbook or script. Possible values are Primary and Recovery. It must not be specified when type is ManualActionDetails. + runbook_id = try(pre_action.value.runbook_id, null) + manual_action_instruction = try(pre_action.value.manual_action_instruction, null) + script_path = try(pre_action.value.script_path, null) + } + } + + dynamic "post_action" { + for_each = can(var.settings.shutdown_recovery_group.post_action) ? [var.settings.shutdown_recovery_group.post_action] : [] + content { + name = post_action.value.name + type = post_action.value.type # (Required) Type of the action detail. Possible values are AutomationRunbookActionDetails, ManualActionDetails and ScriptActionDetails. + fail_over_directions = post_action.value.fail_over_directions # (Required) Directions of fail over. Possible values are PrimaryToRecovery and RecoveryToPrimary + fail_over_types = post_action.value.fail_over_types # (Required) Types of fail over. Possible values are TestFailover, PlannedFailover and UnplannedFailover + fabric_location = try(post_action.value.fabric_location, null) # (Optional) The fabric location of runbook or script. Possible values are Primary and Recovery. It must not be specified when type is ManualActionDetails. + runbook_id = try(post_action.value.runbook_id, null) + manual_action_instruction = try(post_action.value.manual_action_instruction, null) + script_path = try(post_action.value.script_path, null) + } + } + } + } + + dynamic "failover_recovery_group" { + for_each = try(var.settings.failover_recovery_group, null) != null ? [var.settings.failover_recovery_group] : [] + content { + dynamic "pre_action" { + for_each = can(var.settings.failover_recovery_group.pre_action) ? [var.settings.failover_recovery_group.pre_action] : [] + content { + name = pre_action.value.name + type = pre_action.value.type # (Required) Type of the action detail. Possible values are AutomationRunbookActionDetails, ManualActionDetails and ScriptActionDetails. + fail_over_directions = pre_action.value.fail_over_directions # (Required) Directions of fail over. Possible values are PrimaryToRecovery and RecoveryToPrimary + fail_over_types = pre_action.value.fail_over_types # (Required) Types of fail over. Possible values are TestFailover, PlannedFailover and UnplannedFailover + fabric_location = try(pre_action.value.fabric_location, null) # (Optional) The fabric location of runbook or script. Possible values are Primary and Recovery. It must not be specified when type is ManualActionDetails. + runbook_id = try(pre_action.value.runbook_id, null) + manual_action_instruction = try(pre_action.value.manual_action_instruction, null) + script_path = try(pre_action.value.script_path, null) + } + } + + dynamic "post_action" { + for_each = can(var.settings.failover_recovery_group.post_action) ? [var.settings.failover_recovery_group.post_action] : [] + content { + name = post_action.value.name + type = post_action.value.type # (Required) Type of the action detail. Possible values are AutomationRunbookActionDetails, ManualActionDetails and ScriptActionDetails. + fail_over_directions = post_action.value.fail_over_directions # (Required) Directions of fail over. Possible values are PrimaryToRecovery and RecoveryToPrimary + fail_over_types = post_action.value.fail_over_types # (Required) Types of fail over. Possible values are TestFailover, PlannedFailover and UnplannedFailover + fabric_location = try(post_action.value.fabric_location, null) # (Optional) The fabric location of runbook or script. Possible values are Primary and Recovery. It must not be specified when type is ManualActionDetails. + runbook_id = try(post_action.value.runbook_id, null) + manual_action_instruction = try(post_action.value.manual_action_instruction, null) + script_path = try(post_action.value.script_path, null) + } + } + } + } + + dynamic "boot_recovery_group" { + for_each = try(var.settings.boot_recovery_group, null) != null ? [var.settings.boot_recovery_group] : [] + content { + replicated_protected_items = flatten(try( + # [for vm_key in var.settings.boot_recovery_group.virtual_machines_key : var.virtual_machines_replication[var.client_config.landingzone_key][vm_key].id] : + [for vm_key in var.settings.boot_recovery_group.virtual_machines_key : var.virtual_machines_replication[var.client_config.landingzone_key][vm_key].replicated_object_id], + var.settings.boot_recovery_group.replicated_protected_items_id + ) + ) + + dynamic "pre_action" { + # for_each = try(var.settings.boot_recovery_group.value.pre_action, null) != null ? [1] : [] + for_each = can(var.settings.boot_recovery_group.pre_action) ? [var.settings.boot_recovery_group.pre_action] : [] + content { + name = pre_action.value.name + type = pre_action.value.type # (Required) Type of the action detail. Possible values are AutomationRunbookActionDetails, ManualActionDetails and ScriptActionDetails. + fail_over_directions = pre_action.value.fail_over_directions # (Required) Directions of fail over. Possible values are PrimaryToRecovery and RecoveryToPrimary + fail_over_types = pre_action.value.fail_over_types # (Required) Types of fail over. Possible values are TestFailover, PlannedFailover and UnplannedFailover + fabric_location = try(pre_action.value.fabric_location, null) # (Optional) The fabric location of runbook or script. Possible values are Primary and Recovery. It must not be specified when type is ManualActionDetails. + runbook_id = try(pre_action.value.runbook_id, null) + manual_action_instruction = try(pre_action.value.manual_action_instruction, null) + script_path = try(pre_action.value.script_path, null) + } + } + + dynamic "post_action" { + for_each = can(var.settings.boot_recovery_group.post_action) ? [var.settings.boot_recovery_group.post_action] : [] + content { + name = post_action.value.name + type = post_action.value.type # (Required) Type of the action detail. Possible values are AutomationRunbookActionDetails, ManualActionDetails and ScriptActionDetails. + fail_over_directions = post_action.value.fail_over_directions # (Required) Directions of fail over. Possible values are PrimaryToRecovery and RecoveryToPrimary + fail_over_types = post_action.value.fail_over_types # (Required) Types of fail over. Possible values are TestFailover, PlannedFailover and UnplannedFailover + fabric_location = try(post_action.value.fabric_location, null) # (Optional) The fabric location of runbook or script. Possible values are Primary and Recovery. It must not be specified when type is ManualActionDetails. + runbook_id = try(post_action.value.runbook_id, null) + manual_action_instruction = try(post_action.value.manual_action_instruction, null) + script_path = try(post_action.value.script_path, null) + } + } + } + } + + dynamic "azure_to_azure_settings" { + for_each = try(var.settings.azure_to_azure_settings, null) != null ? [var.settings.azure_to_azure_settings] : [] + content { + primary_zone = azure_to_azure_settings.value.primary_zone + recovery_zone = azure_to_azure_settings.value.recovery_zone + } + } +} diff --git a/modules/recovery_vault/recovery_plan/main.tf b/modules/recovery_vault/recovery_plan/main.tf new file mode 100644 index 0000000000..05c3bb16e4 --- /dev/null +++ b/modules/recovery_vault/recovery_plan/main.tf @@ -0,0 +1,23 @@ +locals { + module_tag = { + "module" = basename(abspath(path.module)) + } + tags = var.base_tags ? merge( + var.global_settings.tags, + local.module_tag, + try(var.settings.tags, null) + ) : merge( + local.module_tag, + try(var.settings.tags, + null) + ) +} + + +terraform { + required_providers { + azurecaf = { + source = "aztfmod/azurecaf" + } + } +} diff --git a/modules/recovery_vault/recovery_plan/outputs.tf b/modules/recovery_vault/recovery_plan/outputs.tf new file mode 100644 index 0000000000..a9bee2491a --- /dev/null +++ b/modules/recovery_vault/recovery_plan/outputs.tf @@ -0,0 +1,4 @@ +output "id" { + description = "The IDs of the Recovery Plans." + value = azurerm_site_recovery_replication_recovery_plan.replication_plan.id +} \ No newline at end of file diff --git a/modules/recovery_vault/recovery_plan/variables.tf b/modules/recovery_vault/recovery_plan/variables.tf new file mode 100644 index 0000000000..ca1a9cbb6d --- /dev/null +++ b/modules/recovery_vault/recovery_plan/variables.tf @@ -0,0 +1,25 @@ + +variable "settings" {} +variable "global_settings" { + description = "Global settings object (see module README.md)" +} + +variable "client_config" { + description = "Client configuration object (see module README.md)." +} + +variable "base_tags" { + description = "Base tags for the resource to be inherited from the resource group." + type = bool +} + +variable "virtual_machines_replication" { + default = {} +} + +variable "recovery_fabrics" { + default = {} +} + +variable "recovery_vault_id" { +} diff --git a/modules/recovery_vault/recovery_vault.tf b/modules/recovery_vault/recovery_vault.tf index eecd16a3ac..14803cc86c 100644 --- a/modules/recovery_vault/recovery_vault.tf +++ b/modules/recovery_vault/recovery_vault.tf @@ -1,4 +1,4 @@ -# Tested with : AzureRM version 2.61.0 +# Tested with : AzureRM version 2.99.0 # Ref : https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/recovery_services_vault resource "azurecaf_name" "asr_rg_vault" { @@ -23,7 +23,24 @@ resource "azurerm_recovery_services_vault" "asr" { immutability = try(var.settings.immutability, null) identity { - type = "SystemAssigned" + type = try(var.settings.identity.type, "SystemAssigned") + + # if type contains UserAssigned, `identity_ids` is mandatory + identity_ids = try(regex("UserAssigned", var.settings.identity.type), null) != null ? flatten([ + for managed_identity in var.settings.identity.managed_identities : [ + var.managed_identities[try(managed_identity.lz_key, var.client_config.landingzone_key)][managed_identity.key].id + ] + ]) : null } + dynamic "encryption" { + for_each = can(var.settings.encryption) ? [1] : [] + + content { + key_id = var.settings.encryption.key_id + infrastructure_encryption_enabled = try(var.settings.encryption.infrastructure_encryption_enabled, null) + use_system_assigned_identity = can(var.settings.encryption.user_assigned_identity) ? false : true + user_assigned_identity_id = can(var.settings.encryption.user_assigned_identity) ? var.managed_identities[try(var.settings.encryption.user_assigned_identity.lz_key, var.client_config.landingzone_key)][var.settings.encryption.user_assigned_identity.key].id : null + } + } } diff --git a/modules/recovery_vault/variables.tf b/modules/recovery_vault/variables.tf index aeae2a877a..470bacbde7 100644 --- a/modules/recovery_vault/variables.tf +++ b/modules/recovery_vault/variables.tf @@ -29,4 +29,5 @@ variable "resource_group" { variable "base_tags" { description = "Base tags for the resource to be inherited from the resource group." type = bool -} \ No newline at end of file +} +variable "managed_identities" {} diff --git a/variables.tf b/variables.tf index 4c4d69c4a1..df3df63d73 100644 --- a/variables.tf +++ b/variables.tf @@ -449,4 +449,6 @@ variable "load_test" { description = "Configuration object - Load Test resources" default = {} } - +variable "recovery_plans" { + default = {} +} diff --git a/virtual_machines_replications.tf b/virtual_machines_replications.tf new file mode 100644 index 0000000000..5fe735a757 --- /dev/null +++ b/virtual_machines_replications.tf @@ -0,0 +1,49 @@ +module "vm_replication" { + source = "./modules/compute/virtual_machine_replication" + depends_on = [ + module.virtual_machines, + module.vm_extension_custom_scriptextension, + + ] + + for_each = { + for key, value in try(local.compute.virtual_machines, {}) : key => value + if try(value.replication, null) != null + } + client_config = local.client_config + virtual_machine_id = module.virtual_machines[each.key].id + virtual_machine_name = module.virtual_machines[each.key].name + recovery_vaults = local.combined_objects_recovery_vaults + resource_groups = local.combined_objects_resource_groups + storage_accounts = local.combined_objects_storage_accounts + disk_encryption_sets = local.combined_objects_disk_encryption_sets + settings = each.value + vnets = local.combined_objects_networking + virtual_machine_os_disk = module.virtual_machines[each.key].os_disk + virtual_machine_data_disks = try(module.virtual_machines[each.key].data_disks, null) + virtual_machine_nics = module.virtual_machines[each.key].nics +} + +output "vm_replication" { + value = module.vm_replication +} + +module "recovery_plans" { + source = "./modules/recovery_vault/recovery_plan" + for_each = var.recovery_plans + depends_on = [ + module.vm_replication, + ] + + global_settings = local.global_settings + client_config = local.client_config + settings = each.value + base_tags = local.global_settings.inherit_tags + virtual_machines_replication = local.combined_objects_virtual_machines_replication + recovery_vault_id = module.recovery_vaults[each.value.recovery_vault_key].id + recovery_fabrics = module.recovery_vaults[each.value.recovery_vault_key].recovery_fabrics +} + +output "recovery_plans" { + value = module.recovery_plans +}