diff --git a/data_safe_haven/pulumi/common/__init__.py b/data_safe_haven/pulumi/common/__init__.py
index e69de29bb2..fa5e1a5a32 100644
--- a/data_safe_haven/pulumi/common/__init__.py
+++ b/data_safe_haven/pulumi/common/__init__.py
@@ -0,0 +1,23 @@
+from .enums import NetworkingPriorities
+from .transformations import (
+    get_available_ips_from_subnet,
+    get_id_from_rg,
+    get_id_from_subnet,
+    get_ip_address_from_container_group,
+    get_ip_addresses_from_private_endpoint,
+    get_name_from_rg,
+    get_name_from_subnet,
+    get_name_from_vnet,
+)
+
+__all__ = [
+    "get_available_ips_from_subnet",
+    "get_id_from_rg",
+    "get_id_from_subnet",
+    "get_ip_address_from_container_group",
+    "get_ip_addresses_from_private_endpoint",
+    "get_name_from_rg",
+    "get_name_from_subnet",
+    "get_name_from_vnet",
+    "NetworkingPriorities",
+]
diff --git a/data_safe_haven/pulumi/components/automation_dsc_node.py b/data_safe_haven/pulumi/components/automation_dsc_node.py
index e487c61285..7acffe1ce0 100644
--- a/data_safe_haven/pulumi/components/automation_dsc_node.py
+++ b/data_safe_haven/pulumi/components/automation_dsc_node.py
@@ -55,7 +55,7 @@ def __init__(
         opts: ResourceOptions | None = None,
     ) -> None:
         super().__init__("dsh:common:AutomationDscNode", name, {}, opts)
-        child_opts = ResourceOptions.merge(ResourceOptions(parent=self), opts)
+        child_opts = ResourceOptions.merge(opts, ResourceOptions(parent=self))
 
         # Upload the primary domain controller DSC
         dsc = automation.DscConfiguration(
@@ -77,10 +77,10 @@ def __init__(
                 ),
             ),
             opts=ResourceOptions.merge(
+                child_opts,
                 ResourceOptions(
                     delete_before_replace=True, replace_on_changes=["source.hash"]
                 ),
-                child_opts,
             ),
         )
         dsc_compiled = CompiledDsc(
@@ -97,7 +97,7 @@ def __init__(
                 required_modules=props.dsc_required_modules,
                 subscription_name=props.subscription_name,
             ),
-            opts=ResourceOptions.merge(ResourceOptions(depends_on=[dsc]), child_opts),
+            opts=ResourceOptions.merge(child_opts, ResourceOptions(depends_on=[dsc])),
         )
         compute.VirtualMachineExtension(
             f"{self._name}_dsc_extension",
@@ -130,7 +130,7 @@ def __init__(
             vm_name=props.vm_name,
             vm_extension_name="Microsoft.Powershell.DSC",
             opts=ResourceOptions.merge(
-                ResourceOptions(depends_on=[dsc_compiled]),
                 child_opts,
+                ResourceOptions(depends_on=[dsc_compiled]),
             ),
         )
diff --git a/data_safe_haven/pulumi/components/shm_bastion.py b/data_safe_haven/pulumi/components/shm_bastion.py
index b63b6c44f2..fc627b77df 100644
--- a/data_safe_haven/pulumi/components/shm_bastion.py
+++ b/data_safe_haven/pulumi/components/shm_bastion.py
@@ -29,7 +29,7 @@ def __init__(
         opts: ResourceOptions | None = None,
     ) -> None:
         super().__init__("dsh:shm:BastionComponent", name, {}, opts)
-        child_opts = ResourceOptions.merge(ResourceOptions(parent=self), opts)
+        child_opts = ResourceOptions.merge(opts, ResourceOptions(parent=self))
 
         # Deploy IP address
         public_ip = network.PublicIPAddress(
diff --git a/data_safe_haven/pulumi/components/shm_data.py b/data_safe_haven/pulumi/components/shm_data.py
index fb04442a6c..dc617e4549 100644
--- a/data_safe_haven/pulumi/components/shm_data.py
+++ b/data_safe_haven/pulumi/components/shm_data.py
@@ -54,7 +54,7 @@ def __init__(
         opts: ResourceOptions | None = None,
     ) -> None:
         super().__init__("dsh:shm:DataComponent", name, {}, opts)
-        child_opts = ResourceOptions.merge(ResourceOptions(parent=self), opts)
+        child_opts = ResourceOptions.merge(opts, ResourceOptions(parent=self))
 
         # Deploy resource group
         resource_group = resources.ResourceGroup(
diff --git a/data_safe_haven/pulumi/components/shm_domain_controllers.py b/data_safe_haven/pulumi/components/shm_domain_controllers.py
index 68738787e0..6926c41e3f 100644
--- a/data_safe_haven/pulumi/components/shm_domain_controllers.py
+++ b/data_safe_haven/pulumi/components/shm_domain_controllers.py
@@ -5,7 +5,7 @@
 from pulumi import ComponentResource, Input, Output, ResourceOptions
 from pulumi_azure_native import network, resources
 
-from data_safe_haven.pulumi.common.transformations import get_name_from_subnet
+from data_safe_haven.pulumi.common import get_name_from_subnet
 from data_safe_haven.pulumi.dynamic.remote_powershell import (
     RemoteScript,
     RemoteScriptProps,
@@ -87,7 +87,7 @@ def __init__(
         opts: ResourceOptions | None = None,
     ) -> None:
         super().__init__("dsh:shm:DomainControllersComponent", name, {}, opts)
-        child_opts = ResourceOptions.merge(ResourceOptions(parent=self), opts)
+        child_opts = ResourceOptions.merge(opts, ResourceOptions(parent=self))
         resources_path = pathlib.Path(__file__).parent.parent.parent / "resources"
 
         # Deploy resource group
@@ -157,8 +157,8 @@ def __init__(
                 vm_resource_group_name=resource_group.name,
             ),
             opts=ResourceOptions.merge(
-                ResourceOptions(depends_on=[primary_domain_controller]),
                 child_opts,
+                ResourceOptions(depends_on=[primary_domain_controller]),
             ),
         )
         # Extract the domain SID
@@ -177,13 +177,13 @@ def __init__(
                 vm_resource_group_name=resource_group.name,
             ),
             opts=ResourceOptions.merge(
+                child_opts,
                 ResourceOptions(
                     depends_on=[
                         primary_domain_controller,
                         primary_domain_controller_dsc_node,
                     ]
                 ),
-                child_opts,
             ),
         )
 
diff --git a/data_safe_haven/pulumi/components/shm_firewall.py b/data_safe_haven/pulumi/components/shm_firewall.py
index 5ce5b78c4e..f610a86a74 100644
--- a/data_safe_haven/pulumi/components/shm_firewall.py
+++ b/data_safe_haven/pulumi/components/shm_firewall.py
@@ -2,7 +2,7 @@
 from pulumi import ComponentResource, Input, Output, ResourceOptions
 from pulumi_azure_native import network
 
-from data_safe_haven.pulumi.common.transformations import get_id_from_subnet
+from data_safe_haven.pulumi.common import get_id_from_subnet
 
 
 class SHMFirewallProps:
@@ -46,7 +46,7 @@ def __init__(
         opts: ResourceOptions | None = None,
     ) -> None:
         super().__init__("dsh:shm:FirewallComponent", name, {}, opts)
-        child_opts = ResourceOptions.merge(ResourceOptions(parent=self), opts)
+        child_opts = ResourceOptions.merge(opts, ResourceOptions(parent=self))
 
         # Important IP addresses
         # https://docs.microsoft.com/en-us/azure/virtual-network/what-is-ip-address-168-63-129-16
diff --git a/data_safe_haven/pulumi/components/shm_monitoring.py b/data_safe_haven/pulumi/components/shm_monitoring.py
index 037ab425d0..82cecddf77 100644
--- a/data_safe_haven/pulumi/components/shm_monitoring.py
+++ b/data_safe_haven/pulumi/components/shm_monitoring.py
@@ -14,7 +14,7 @@
     replace_separators,
     time_as_string,
 )
-from data_safe_haven.pulumi.common.transformations import get_id_from_subnet
+from data_safe_haven.pulumi.common import get_id_from_subnet
 
 
 class SHMMonitoringProps:
@@ -48,7 +48,7 @@ def __init__(
         opts: ResourceOptions | None = None,
     ) -> None:
         super().__init__("dsh:shm:MonitoringComponent", name, {}, opts)
-        child_opts = ResourceOptions.merge(ResourceOptions(parent=self), opts)
+        child_opts = ResourceOptions.merge(opts, ResourceOptions(parent=self))
 
         # Deploy resource group
         resource_group = resources.ResourceGroup(
@@ -285,8 +285,8 @@ def __init__(
                 ),
             ),
             opts=ResourceOptions.merge(
-                ResourceOptions(ignore_changes=["schedule_info"]),
                 child_opts,
+                ResourceOptions(ignore_changes=["schedule_info"]),
             ),
         )
         # Create Windows VM system update schedule: daily at 02:02
@@ -333,13 +333,13 @@ def __init__(
                 ),
             ),
             opts=ResourceOptions.merge(
+                child_opts,
                 ResourceOptions(
                     ignore_changes=[
                         "schedule_info",  # options are added after deployment
                         "updateConfiguration.windows.included_package_classifications",  # ordering might change
                     ]
                 ),
-                child_opts,
             ),
         )
         # Create Linux VM system update schedule: daily at 02:02
@@ -383,13 +383,13 @@ def __init__(
                 ),
             ),
             opts=ResourceOptions.merge(
+                child_opts,
                 ResourceOptions(
                     ignore_changes=[
                         "schedule_info",  # options are added after deployment
                         "updateConfiguration.linux.included_package_classifications",  # ordering might change
                     ]
                 ),
-                child_opts,
             ),
         )
 
diff --git a/data_safe_haven/pulumi/components/shm_networking.py b/data_safe_haven/pulumi/components/shm_networking.py
index 7c2b7d224a..837588444c 100644
--- a/data_safe_haven/pulumi/components/shm_networking.py
+++ b/data_safe_haven/pulumi/components/shm_networking.py
@@ -6,7 +6,7 @@
 
 from data_safe_haven.external import AzureIPv4Range
 from data_safe_haven.functions import ordered_private_dns_zones
-from data_safe_haven.pulumi.common.enums import NetworkingPriorities
+from data_safe_haven.pulumi.common import NetworkingPriorities
 
 
 class SHMNetworkingProps:
@@ -47,7 +47,7 @@ def __init__(
         opts: ResourceOptions | None = None,
     ) -> None:
         super().__init__("dsh:shm:NetworkingComponent", name, {}, opts)
-        child_opts = ResourceOptions.merge(ResourceOptions(parent=self), opts)
+        child_opts = ResourceOptions.merge(opts, ResourceOptions(parent=self))
 
         # Deploy resource group
         resource_group = resources.ResourceGroup(
@@ -325,10 +325,10 @@ def __init__(
             route_table_name=f"{stack_name}-route",
             routes=[],
             opts=ResourceOptions.merge(
+                child_opts,
                 ResourceOptions(
                     ignore_changes=["routes"]
                 ),  # allow routes to be created outside this definition
-                child_opts,
             ),
         )
 
@@ -392,10 +392,10 @@ def __init__(
             virtual_network_name=f"{stack_name}-vnet",
             virtual_network_peerings=[],
             opts=ResourceOptions.merge(
+                child_opts,
                 ResourceOptions(
                     ignore_changes=["virtual_network_peerings"]
                 ),  # allow SRE virtual networks to peer to this
-                child_opts,
             ),
         )
 
diff --git a/data_safe_haven/pulumi/components/shm_update_servers.py b/data_safe_haven/pulumi/components/shm_update_servers.py
index fbdc4684c5..70e813f63a 100644
--- a/data_safe_haven/pulumi/components/shm_update_servers.py
+++ b/data_safe_haven/pulumi/components/shm_update_servers.py
@@ -5,7 +5,7 @@
 from pulumi_azure_native import network
 
 from data_safe_haven.functions import b64encode
-from data_safe_haven.pulumi.common.transformations import (
+from data_safe_haven.pulumi.common import (
     get_available_ips_from_subnet,
     get_name_from_subnet,
 )
@@ -53,7 +53,7 @@ def __init__(
         opts: ResourceOptions | None = None,
     ) -> None:
         super().__init__("dsh:shm:UpdateServersComponent", name, {}, opts)
-        child_opts = ResourceOptions.merge(ResourceOptions(parent=self), opts)
+        child_opts = ResourceOptions.merge(opts, ResourceOptions(parent=self))
 
         # Load cloud-init file
         b64cloudinit = self.read_cloudinit()
diff --git a/data_safe_haven/pulumi/components/sre_application_gateway.py b/data_safe_haven/pulumi/components/sre_application_gateway.py
index 9e1b45c30e..90b75df1d0 100644
--- a/data_safe_haven/pulumi/components/sre_application_gateway.py
+++ b/data_safe_haven/pulumi/components/sre_application_gateway.py
@@ -4,7 +4,7 @@
 from pulumi import ComponentResource, Input, Output, ResourceOptions
 from pulumi_azure_native import managedidentity, network, resources
 
-from data_safe_haven.pulumi.common.transformations import (
+from data_safe_haven.pulumi.common import (
     get_available_ips_from_subnet,
     get_id_from_rg,
     get_id_from_subnet,
@@ -55,7 +55,7 @@ def __init__(
         opts: ResourceOptions | None = None,
     ) -> None:
         super().__init__("dsh:sre:ApplicationGatewayComponent", name, {}, opts)
-        child_opts = ResourceOptions.merge(ResourceOptions(parent=self), opts)
+        child_opts = ResourceOptions.merge(opts, ResourceOptions(parent=self))
 
         # Define public IP address
         public_ip = network.PublicIPAddress(
diff --git a/data_safe_haven/pulumi/components/sre_data.py b/data_safe_haven/pulumi/components/sre_data.py
index af1fd8ed59..6f5897557d 100644
--- a/data_safe_haven/pulumi/components/sre_data.py
+++ b/data_safe_haven/pulumi/components/sre_data.py
@@ -19,7 +19,7 @@
     sha256hash,
     truncate_tokens,
 )
-from data_safe_haven.pulumi.common.transformations import (
+from data_safe_haven.pulumi.common import (
     get_id_from_subnet,
     get_name_from_rg,
 )
@@ -105,7 +105,7 @@ def __init__(
         opts: ResourceOptions | None = None,
     ) -> None:
         super().__init__("dsh:sre:DataComponent", name, {}, opts)
-        child_opts = ResourceOptions.merge(ResourceOptions(parent=self), opts)
+        child_opts = ResourceOptions.merge(opts, ResourceOptions(parent=self))
 
         # Deploy resource group
         resource_group = resources.ResourceGroup(
@@ -223,10 +223,11 @@ def __init__(
                 subscription_name=props.subscription_name,
             ),
             opts=ResourceOptions.merge(
+                child_opts,
                 ResourceOptions(
-                    depends_on=[props.dns_record]
+                    depends_on=[props.dns_record],
+                    parent=key_vault,
                 ),  # we need the delegation NS record to exist before generating the certificate
-                ResourceOptions(parent=key_vault),
             ),
         )
 
@@ -239,7 +240,7 @@ def __init__(
             resource_group_name=resource_group.name,
             secret_name="password-workspace-admin",
             vault_name=key_vault.name,
-            opts=ResourceOptions(parent=key_vault),
+            opts=ResourceOptions.merge(child_opts, ResourceOptions(parent=key_vault)),
         )
         keyvault.Secret(
             f"{self._name}_kvs_password_gitea_database_admin",
@@ -249,7 +250,7 @@ def __init__(
             resource_group_name=resource_group.name,
             secret_name="password-gitea-database-admin",
             vault_name=key_vault.name,
-            opts=ResourceOptions(parent=key_vault),
+            opts=ResourceOptions.merge(child_opts, ResourceOptions(parent=key_vault)),
         )
         keyvault.Secret(
             f"{self._name}_kvs_password_hedgedoc_database_admin",
@@ -259,7 +260,7 @@ def __init__(
             resource_group_name=resource_group.name,
             secret_name="password-hedgedoc-database-admin",
             vault_name=key_vault.name,
-            opts=ResourceOptions(parent=key_vault),
+            opts=ResourceOptions.merge(child_opts, ResourceOptions(parent=key_vault)),
         )
         keyvault.Secret(
             f"{self._name}_kvs_password_nexus_admin",
@@ -267,7 +268,7 @@ def __init__(
             resource_group_name=resource_group.name,
             secret_name="password-nexus-admin",
             vault_name=key_vault.name,
-            opts=ResourceOptions(parent=key_vault),
+            opts=ResourceOptions.merge(child_opts, ResourceOptions(parent=key_vault)),
         )
         keyvault.Secret(
             f"{self._name}_kvs_password_user_database_admin",
@@ -277,7 +278,7 @@ def __init__(
             resource_group_name=resource_group.name,
             secret_name="password-user-database-admin",
             vault_name=key_vault.name,
-            opts=ResourceOptions(parent=key_vault),
+            opts=ResourceOptions.merge(child_opts, ResourceOptions(parent=key_vault)),
         )
 
         # Deploy state storage account
@@ -363,7 +364,9 @@ def __init__(
                 "/providers/Microsoft.Authorization/roleDefinitions/b7e6dc6d-f1e8-4753-8033-0f276bb0955b",
             ),
             scope=storage_account_securedata.id,
-            opts=ResourceOptions(parent=storage_account_securedata),
+            opts=ResourceOptions.merge(
+                child_opts, ResourceOptions(parent=storage_account_securedata)
+            ),
         )
         # Deploy storage containers
         storage_container_egress = storage.BlobContainer(
@@ -374,7 +377,9 @@ def __init__(
             deny_encryption_scope_override=False,
             public_access=storage.PublicAccess.NONE,
             resource_group_name=resource_group.name,
-            opts=ResourceOptions(parent=storage_account_securedata),
+            opts=ResourceOptions.merge(
+                child_opts, ResourceOptions(parent=storage_account_securedata)
+            ),
         )
         storage_container_ingress = storage.BlobContainer(
             f"{self._name}_storage_container_ingress",
@@ -384,7 +389,9 @@ def __init__(
             deny_encryption_scope_override=False,
             public_access=storage.PublicAccess.NONE,
             resource_group_name=resource_group.name,
-            opts=ResourceOptions(parent=storage_account_securedata),
+            opts=ResourceOptions.merge(
+                child_opts, ResourceOptions(parent=storage_account_securedata)
+            ),
         )
         # Set storage container ACLs
         BlobContainerAcl(
@@ -402,7 +409,9 @@ def __init__(
                 storage_account_name=storage_account_securedata.name,
                 subscription_name=props.subscription_name,
             ),
-            opts=ResourceOptions(parent=storage_container_egress),
+            opts=ResourceOptions.merge(
+                child_opts, ResourceOptions(parent=storage_container_egress)
+            ),
         )
         BlobContainerAcl(
             f"{self._name}_storage_container_ingress_acl",
@@ -418,7 +427,9 @@ def __init__(
                 storage_account_name=storage_account_securedata.name,
                 subscription_name=props.subscription_name,
             ),
-            opts=ResourceOptions(parent=storage_container_ingress),
+            opts=ResourceOptions.merge(
+                child_opts, ResourceOptions(parent=storage_container_ingress)
+            ),
         )
         # Set up a private endpoint for the securedata data account
         storage_account_securedata_endpoint = network.PrivateEndpoint(
@@ -434,7 +445,9 @@ def __init__(
             ],
             resource_group_name=resource_group.name,
             subnet=network.SubnetArgs(id=props.subnet_private_data_id),
-            opts=ResourceOptions(parent=storage_account_securedata),
+            opts=ResourceOptions.merge(
+                child_opts, ResourceOptions(parent=storage_account_securedata)
+            ),
         )
         # Add a private DNS record for each securedata data custom DNS config
         network.PrivateDnsZoneGroup(
@@ -454,7 +467,9 @@ def __init__(
             private_dns_zone_group_name=f"{stack_name}-dzg-storage-account-securedata",
             private_endpoint_name=storage_account_securedata_endpoint.name,
             resource_group_name=resource_group.name,
-            opts=ResourceOptions(parent=storage_account_securedata),
+            opts=ResourceOptions.merge(
+                child_opts, ResourceOptions(parent=storage_account_securedata)
+            ),
         )
 
         # Deploy userdata files storage account
@@ -502,7 +517,9 @@ def __init__(
             root_squash=storage.RootSquashType.NO_ROOT_SQUASH,
             share_name="home",
             share_quota=1024,
-            opts=ResourceOptions(parent=storage_account_userdata),
+            opts=ResourceOptions.merge(
+                child_opts, ResourceOptions(parent=storage_account_userdata)
+            ),
         )
         storage.FileShare(
             f"{self._name}_storage_container_shared",
@@ -513,7 +530,9 @@ def __init__(
             root_squash=storage.RootSquashType.ROOT_SQUASH,
             share_name="shared",
             share_quota=1024,
-            opts=ResourceOptions(parent=storage_account_userdata),
+            opts=ResourceOptions.merge(
+                child_opts, ResourceOptions(parent=storage_account_userdata)
+            ),
         )
         # Set up a private endpoint for the userdata storage account
         storage_account_userdata_endpoint = network.PrivateEndpoint(
@@ -529,7 +548,9 @@ def __init__(
             ],
             resource_group_name=resource_group.name,
             subnet=network.SubnetArgs(id=props.subnet_private_data_id),
-            opts=ResourceOptions(parent=storage_account_userdata),
+            opts=ResourceOptions.merge(
+                child_opts, ResourceOptions(parent=storage_account_userdata)
+            ),
         )
         # Add a private DNS record for each userdata custom DNS config
         network.PrivateDnsZoneGroup(
@@ -548,7 +569,9 @@ def __init__(
             private_dns_zone_group_name=f"{stack_name}-dzg-storage-account-userdata",
             private_endpoint_name=storage_account_userdata_endpoint.name,
             resource_group_name=resource_group.name,
-            opts=child_opts,
+            opts=ResourceOptions.merge(
+                child_opts, ResourceOptions(parent=storage_account_userdata)
+            ),
         )
 
         # Register outputs
diff --git a/data_safe_haven/pulumi/components/sre_gitea_server.py b/data_safe_haven/pulumi/components/sre_gitea_server.py
index 18095eb41a..2fd244db4c 100644
--- a/data_safe_haven/pulumi/components/sre_gitea_server.py
+++ b/data_safe_haven/pulumi/components/sre_gitea_server.py
@@ -3,7 +3,7 @@
 from pulumi import ComponentResource, Input, Output, ResourceOptions
 from pulumi_azure_native import containerinstance, dbforpostgresql, network, storage
 
-from data_safe_haven.pulumi.common.transformations import (
+from data_safe_haven.pulumi.common import (
     get_ip_address_from_container_group,
     get_ip_addresses_from_private_endpoint,
 )
@@ -75,7 +75,7 @@ def __init__(
         opts: ResourceOptions | None = None,
     ) -> None:
         super().__init__("dsh:sre:GiteaServerComponent", name, {}, opts)
-        child_opts = ResourceOptions.merge(ResourceOptions(parent=self), opts)
+        child_opts = ResourceOptions.merge(opts, ResourceOptions(parent=self))
 
         # Define configuration file shares
         file_share_gitea_caddy = storage.FileShare(
@@ -113,7 +113,9 @@ def __init__(
                 storage_account_key=props.storage_account_key,
                 storage_account_name=props.storage_account_name,
             ),
-            opts=child_opts,
+            opts=ResourceOptions.merge(
+                child_opts, ResourceOptions(parent=file_share_gitea_caddy)
+            ),
         )
 
         # Upload Gitea configuration script
@@ -143,7 +145,9 @@ def __init__(
                 storage_account_key=props.storage_account_key,
                 storage_account_name=props.storage_account_name,
             ),
-            opts=child_opts,
+            opts=ResourceOptions.merge(
+                child_opts, ResourceOptions(parent=file_share_gitea_gitea)
+            ),
         )
         # Upload Gitea entrypoint script
         gitea_entrypoint_sh_reader = FileReader(
@@ -158,7 +162,9 @@ def __init__(
                 storage_account_key=props.storage_account_key,
                 storage_account_name=props.storage_account_name,
             ),
-            opts=child_opts,
+            opts=ResourceOptions.merge(
+                child_opts, ResourceOptions(parent=file_share_gitea_gitea)
+            ),
         )
 
         # Define a PostgreSQL server and default database
@@ -198,7 +204,9 @@ def __init__(
             database_name=gitea_db_database_name,
             resource_group_name=props.user_services_resource_group_name,
             server_name=gitea_db_server.name,
-            opts=ResourceOptions(parent=gitea_db_server),
+            opts=ResourceOptions.merge(
+                child_opts, ResourceOptions(parent=gitea_db_server)
+            ),
         )
         # Deploy a private endpoint to the PostgreSQL server
         gitea_db_private_endpoint = network.PrivateEndpoint(
@@ -218,7 +226,9 @@ def __init__(
             ],
             resource_group_name=props.user_services_resource_group_name,
             subnet=network.SubnetArgs(id=props.database_subnet_id),
-            opts=child_opts,
+            opts=ResourceOptions.merge(
+                child_opts, ResourceOptions(parent=gitea_db_server)
+            ),
         )
         gitea_db_private_ip_address = Output.from_input(
             get_ip_addresses_from_private_endpoint(gitea_db_private_endpoint)
@@ -351,6 +361,7 @@ def __init__(
                 ),
             ],
             opts=ResourceOptions.merge(
+                child_opts,
                 ResourceOptions(
                     delete_before_replace=True,
                     depends_on=[
@@ -360,11 +371,10 @@ def __init__(
                     ],
                     replace_on_changes=["containers"],
                 ),
-                child_opts,
             ),
         )
         # Register the container group in the SRE private DNS zone
-        network.PrivateRecordSet(
+        private_dns_record_set = network.PrivateRecordSet(
             f"{self._name}_gitea_private_record_set",
             a_records=[
                 network.ARecordArgs(
@@ -376,7 +386,9 @@ def __init__(
             relative_record_set_name="gitea",
             resource_group_name=props.networking_resource_group_name,
             ttl=3600,
-            opts=child_opts,
+            opts=ResourceOptions.merge(
+                child_opts, ResourceOptions(parent=container_group)
+            ),
         )
         # Redirect the public DNS to private DNS
         network.RecordSet(
@@ -389,5 +401,7 @@ def __init__(
             resource_group_name=props.networking_resource_group_name,
             ttl=3600,
             zone_name=props.sre_fqdn,
-            opts=child_opts,
+            opts=ResourceOptions.merge(
+                child_opts, ResourceOptions(parent=private_dns_record_set)
+            ),
         )
diff --git a/data_safe_haven/pulumi/components/sre_hedgedoc_server.py b/data_safe_haven/pulumi/components/sre_hedgedoc_server.py
index a144137919..64ef209d0a 100644
--- a/data_safe_haven/pulumi/components/sre_hedgedoc_server.py
+++ b/data_safe_haven/pulumi/components/sre_hedgedoc_server.py
@@ -4,7 +4,7 @@
 from pulumi_azure_native import containerinstance, dbforpostgresql, network, storage
 
 from data_safe_haven.functions import b64encode
-from data_safe_haven.pulumi.common.transformations import (
+from data_safe_haven.pulumi.common import (
     get_ip_address_from_container_group,
     get_ip_addresses_from_private_endpoint,
 )
@@ -88,7 +88,7 @@ def __init__(
         opts: ResourceOptions | None = None,
     ) -> None:
         super().__init__("dsh:sre:HedgeDocServerComponent", name, {}, opts)
-        child_opts = ResourceOptions.merge(ResourceOptions(parent=self), opts)
+        child_opts = ResourceOptions.merge(opts, ResourceOptions(parent=self))
 
         # Define configuration file shares
         file_share_hedgedoc_caddy = storage.FileShare(
@@ -117,7 +117,9 @@ def __init__(
                 storage_account_key=props.storage_account_key,
                 storage_account_name=props.storage_account_name,
             ),
-            opts=child_opts,
+            opts=ResourceOptions.merge(
+                child_opts, ResourceOptions(parent=file_share_hedgedoc_caddy)
+            ),
         )
 
         # Load HedgeDoc configuration file for later use
@@ -162,7 +164,9 @@ def __init__(
             database_name=hedgedoc_db_database_name,
             resource_group_name=props.user_services_resource_group_name,
             server_name=hedgedoc_db_server.name,
-            opts=ResourceOptions(parent=hedgedoc_db_server),
+            opts=ResourceOptions.merge(
+                child_opts, ResourceOptions(parent=hedgedoc_db_server)
+            ),
         )
         # Deploy a private endpoint to the PostgreSQL server
         hedgedoc_db_private_endpoint = network.PrivateEndpoint(
@@ -182,7 +186,9 @@ def __init__(
             ],
             resource_group_name=props.user_services_resource_group_name,
             subnet=network.SubnetArgs(id=props.database_subnet_id),
-            opts=child_opts,
+            opts=ResourceOptions.merge(
+                child_opts, ResourceOptions(parent=hedgedoc_db_server)
+            ),
         )
         hedgedoc_db_private_ip_address = Output.from_input(
             get_ip_addresses_from_private_endpoint(hedgedoc_db_private_endpoint)
@@ -348,6 +354,7 @@ def __init__(
                 ),
             ],
             opts=ResourceOptions.merge(
+                child_opts,
                 ResourceOptions(
                     delete_before_replace=True,
                     depends_on=[
@@ -355,11 +362,10 @@ def __init__(
                     ],
                     replace_on_changes=["containers"],
                 ),
-                child_opts,
             ),
         )
         # Register the container group in the SRE private DNS zone
-        network.PrivateRecordSet(
+        private_dns_record_set = network.PrivateRecordSet(
             f"{self._name}_hedgedoc_private_record_set",
             a_records=[
                 network.ARecordArgs(
@@ -371,7 +377,9 @@ def __init__(
             relative_record_set_name="hedgedoc",
             resource_group_name=props.networking_resource_group_name,
             ttl=3600,
-            opts=child_opts,
+            opts=ResourceOptions.merge(
+                child_opts, ResourceOptions(parent=container_group)
+            ),
         )
         # Redirect the public DNS to private DNS
         network.RecordSet(
@@ -384,5 +392,7 @@ def __init__(
             resource_group_name=props.networking_resource_group_name,
             ttl=3600,
             zone_name=props.sre_fqdn,
-            opts=child_opts,
+            opts=ResourceOptions.merge(
+                child_opts, ResourceOptions(parent=private_dns_record_set)
+            ),
         )
diff --git a/data_safe_haven/pulumi/components/sre_monitoring.py b/data_safe_haven/pulumi/components/sre_monitoring.py
index 6ebf02faf1..82aea96e23 100644
--- a/data_safe_haven/pulumi/components/sre_monitoring.py
+++ b/data_safe_haven/pulumi/components/sre_monitoring.py
@@ -36,7 +36,7 @@ def __init__(
         opts: ResourceOptions | None = None,
     ) -> None:
         super().__init__("dsh:sre:MonitoringComponent", name, {}, opts)
-        child_opts = ResourceOptions.merge(ResourceOptions(parent=self), opts)
+        child_opts = ResourceOptions.merge(opts, ResourceOptions(parent=self))
 
         # Create Linux VM system update schedule: daily at 03:<index>
         automation.SoftwareUpdateConfigurationByName(
@@ -85,12 +85,12 @@ def __init__(
                 ),
             ),
             opts=ResourceOptions.merge(
+                child_opts,
                 ResourceOptions(
                     ignore_changes=[
                         "schedule_info",  # options are added after deployment
                         "updateConfiguration.linux.included_package_classifications",  # ordering might change
                     ]
                 ),
-                child_opts,
             ),
         )
diff --git a/data_safe_haven/pulumi/components/sre_networking.py b/data_safe_haven/pulumi/components/sre_networking.py
index 927ed0ea04..9117eb103e 100644
--- a/data_safe_haven/pulumi/components/sre_networking.py
+++ b/data_safe_haven/pulumi/components/sre_networking.py
@@ -4,7 +4,7 @@
 
 from data_safe_haven.external import AzureIPv4Range
 from data_safe_haven.functions import alphanumeric, ordered_private_dns_zones
-from data_safe_haven.pulumi.common.enums import NetworkingPriorities
+from data_safe_haven.pulumi.common import NetworkingPriorities
 
 
 class SRENetworkingProps:
@@ -31,23 +31,23 @@ def __init__(
             lambda r: r.next_subnet(256)
         )
         self.subnet_guacamole_containers_iprange = self.vnet_iprange.apply(
-            lambda r: r.next_subnet(128)
+            lambda r: r.next_subnet(8)
         )
-        self.subnet_guacamole_database_iprange = self.vnet_iprange.apply(
-            lambda r: r.next_subnet(128)
+        self.subnet_guacamole_containers_support_iprange = self.vnet_iprange.apply(
+            lambda r: r.next_subnet(8)
         )
         self.subnet_private_data_iprange = self.vnet_iprange.apply(
             lambda r: r.next_subnet(16)
         )
-        self.subnet_software_repositories_iprange = self.vnet_iprange.apply(
-            lambda r: r.next_subnet(8)
-        )
         self.subnet_user_services_containers_iprange = self.vnet_iprange.apply(
             lambda r: r.next_subnet(8)
         )
-        self.subnet_user_services_databases_iprange = self.vnet_iprange.apply(
+        self.subnet_user_services_containers_support_iprange = self.vnet_iprange.apply(
             lambda r: r.next_subnet(8)
         )
+        self.subnet_user_services_software_repositories_iprange = (
+            self.vnet_iprange.apply(lambda r: r.next_subnet(8))
+        )
         self.subnet_workspaces_iprange = self.vnet_iprange.apply(
             lambda r: r.next_subnet(256)
         )
@@ -75,7 +75,7 @@ def __init__(
         opts: ResourceOptions | None = None,
     ) -> None:
         super().__init__("dsh:sre:NetworkingComponent", name, {}, opts)
-        child_opts = ResourceOptions.merge(ResourceOptions(parent=self), opts)
+        child_opts = ResourceOptions.merge(opts, ResourceOptions(parent=self))
 
         # Deploy resource group
         resource_group = resources.ResourceGroup(
@@ -92,20 +92,24 @@ def __init__(
         subnet_guacamole_containers_prefix = (
             props.subnet_guacamole_containers_iprange.apply(lambda r: str(r))
         )
-        subnet_guacamole_database_prefix = (
-            props.subnet_guacamole_database_iprange.apply(lambda r: str(r))
+        subnet_guacamole_containers_support_prefix = (
+            props.subnet_guacamole_containers_support_iprange.apply(lambda r: str(r))
         )
         subnet_private_data_prefix = props.subnet_private_data_iprange.apply(
             lambda r: str(r)
         )
-        subnet_software_repositories_prefix = (
-            props.subnet_software_repositories_iprange.apply(lambda r: str(r))
-        )
         subnet_user_services_containers_prefix = (
             props.subnet_user_services_containers_iprange.apply(lambda r: str(r))
         )
-        subnet_user_services_databases_prefix = (
-            props.subnet_user_services_databases_iprange.apply(lambda r: str(r))
+        subnet_user_services_containers_support_prefix = (
+            props.subnet_user_services_containers_support_iprange.apply(
+                lambda r: str(r)
+            )
+        )
+        subnet_user_services_software_repositories_prefix = (
+            props.subnet_user_services_software_repositories_iprange.apply(
+                lambda r: str(r)
+            )
         )
         subnet_workspaces_prefix = props.subnet_workspaces_iprange.apply(
             lambda r: str(r)
@@ -162,9 +166,9 @@ def __init__(
             resource_group_name=resource_group.name,
             opts=child_opts,
         )
-        nsg_guacamole_database = network.NetworkSecurityGroup(
-            f"{self._name}_nsg_guacamole_database",
-            network_security_group_name=f"{stack_name}-nsg-guacamole-database",
+        nsg_guacamole_containers_support = network.NetworkSecurityGroup(
+            f"{self._name}_nsg_guacamole_containers_support",
+            network_security_group_name=f"{stack_name}-nsg-guacamole-containers-support",
             resource_group_name=resource_group.name,
             opts=child_opts,
         )
@@ -174,21 +178,21 @@ def __init__(
             resource_group_name=resource_group.name,
             opts=child_opts,
         )
-        nsg_software_repositories = network.NetworkSecurityGroup(
-            f"{self._name}_nsg_software_repositories",
-            network_security_group_name=f"{stack_name}-nsg-software-repositories",
-            resource_group_name=resource_group.name,
-            opts=child_opts,
-        )
         nsg_user_services_containers = network.NetworkSecurityGroup(
             f"{self._name}_nsg_user_services_containers",
             network_security_group_name=f"{stack_name}-nsg-user-services-containers",
             resource_group_name=resource_group.name,
             opts=child_opts,
         )
-        nsg_user_services_databases = network.NetworkSecurityGroup(
-            f"{self._name}_nsg_user_services_databases",
-            network_security_group_name=f"{stack_name}-nsg-user-services-databases",
+        nsg_user_services_containers_support = network.NetworkSecurityGroup(
+            f"{self._name}_nsg_user_services_containers_support",
+            network_security_group_name=f"{stack_name}-nsg-user-services-containers-support",
+            resource_group_name=resource_group.name,
+            opts=child_opts,
+        )
+        nsg_user_services_software_repositories = network.NetworkSecurityGroup(
+            f"{self._name}_nsg_user_services_software_repositories",
+            network_security_group_name=f"{stack_name}-nsg-user-services-software-repositories",
             resource_group_name=resource_group.name,
             opts=child_opts,
         )
@@ -305,11 +309,15 @@ def __init__(
         # Define the virtual network and its subnets
         subnet_application_gateway_name = "ApplicationGatewaySubnet"
         subnet_guacamole_containers_name = "GuacamoleContainersSubnet"
-        subnet_guacamole_database_name = "GuacamoleDatabaseSubnet"
+        subnet_guacamole_containers_support_name = "GuacamoleContainersSupportSubnet"
         subnet_private_data_name = "PrivateDataSubnet"
-        subnet_software_repositories_name = "SoftwareRepositoriesSubnet"
         subnet_user_services_containers_name = "UserServicesContainersSubnet"
-        subnet_user_services_databases_name = "UserServicesDatabasesSubnet"
+        subnet_user_services_containers_support_name = (
+            "UserServicesContainersSupportSubnet"
+        )
+        subnet_user_services_software_repositories_name = (
+            "UserServicesSoftwareRepositoriesSubnet"
+        )
         subnet_workspaces_name = "WorkspacesSubnet"
         sre_virtual_network = network.VirtualNetwork(
             f"{self._name}_virtual_network",
@@ -341,12 +349,12 @@ def __init__(
                         id=nsg_guacamole_containers.id
                     ),
                 ),
-                # Guacamole database
+                # Guacamole containers support
                 network.SubnetArgs(
-                    address_prefix=subnet_guacamole_database_prefix,
-                    name=subnet_guacamole_database_name,
+                    address_prefix=subnet_guacamole_containers_support_prefix,
+                    name=subnet_guacamole_containers_support_name,
                     network_security_group=network.NetworkSecurityGroupArgs(
-                        id=nsg_guacamole_database.id
+                        id=nsg_guacamole_containers_support.id
                     ),
                     private_endpoint_network_policies="Disabled",
                 ),
@@ -364,9 +372,9 @@ def __init__(
                         )
                     ],
                 ),
-                # Software repositories
+                # User services containers
                 network.SubnetArgs(
-                    address_prefix=subnet_software_repositories_prefix,
+                    address_prefix=subnet_user_services_containers_prefix,
                     delegations=[
                         network.DelegationArgs(
                             name="SubnetDelegationContainerGroups",
@@ -374,14 +382,22 @@ def __init__(
                             type="Microsoft.Network/virtualNetworks/subnets/delegations",
                         ),
                     ],
-                    name=subnet_software_repositories_name,
+                    name=subnet_user_services_containers_name,
                     network_security_group=network.NetworkSecurityGroupArgs(
-                        id=nsg_software_repositories.id
+                        id=nsg_user_services_containers.id
                     ),
                 ),
-                # User services containers
+                # User services containers support
                 network.SubnetArgs(
-                    address_prefix=subnet_user_services_containers_prefix,
+                    address_prefix=subnet_user_services_containers_support_prefix,
+                    name=subnet_user_services_containers_support_name,
+                    network_security_group=network.NetworkSecurityGroupArgs(
+                        id=nsg_user_services_containers_support.id
+                    ),
+                ),
+                # User services software repositories
+                network.SubnetArgs(
+                    address_prefix=subnet_user_services_software_repositories_prefix,
                     delegations=[
                         network.DelegationArgs(
                             name="SubnetDelegationContainerGroups",
@@ -389,17 +405,9 @@ def __init__(
                             type="Microsoft.Network/virtualNetworks/subnets/delegations",
                         ),
                     ],
-                    name=subnet_user_services_containers_name,
-                    network_security_group=network.NetworkSecurityGroupArgs(
-                        id=nsg_user_services_containers.id
-                    ),
-                ),
-                # User services databases
-                network.SubnetArgs(
-                    address_prefix=subnet_user_services_databases_prefix,
-                    name=subnet_user_services_databases_name,
+                    name=subnet_user_services_software_repositories_name,
                     network_security_group=network.NetworkSecurityGroupArgs(
-                        id=nsg_user_services_databases.id
+                        id=nsg_user_services_software_repositories.id
                     ),
                 ),
                 # Workspaces
@@ -414,10 +422,10 @@ def __init__(
             virtual_network_name=f"{stack_name}-vnet",
             virtual_network_peerings=[],
             opts=ResourceOptions.merge(
+                child_opts,
                 ResourceOptions(
                     ignore_changes=["virtual_network_peerings"]
                 ),  # allow peering to SHM virtual network
-                child_opts,
             ),
         )
 
@@ -439,7 +447,9 @@ def __init__(
             virtual_network_peering_name=Output.concat(
                 "peer_sre_", props.sre_name, "_to_shm"
             ),
-            opts=child_opts,
+            opts=ResourceOptions.merge(
+                child_opts, ResourceOptions(parent=sre_virtual_network)
+            ),
         )
         network.VirtualNetworkPeering(
             f"{self._name}_shm_to_sre_peering",
@@ -450,7 +460,9 @@ def __init__(
             virtual_network_peering_name=Output.concat(
                 "peer_shm_to_sre_", props.sre_name
             ),
-            opts=child_opts,
+            opts=ResourceOptions.merge(
+                child_opts, ResourceOptions(parent=sre_virtual_network)
+            ),
         )
 
         # Link to SHM private DNS zones
@@ -465,7 +477,9 @@ def __init__(
                 virtual_network_link_name=Output.concat(
                     "link-to-", sre_virtual_network.name
                 ),
-                opts=child_opts,
+                opts=ResourceOptions.merge(
+                    child_opts, ResourceOptions(parent=sre_virtual_network)
+                ),
             )
 
         # Define SRE DNS zone
@@ -500,7 +514,9 @@ def __init__(
             resource_group_name=props.shm_networking_resource_group_name,
             ttl=3600,
             zone_name=shm_dns_zone.name,
-            opts=child_opts,
+            opts=ResourceOptions.merge(
+                child_opts, ResourceOptions(parent=sre_dns_zone)
+            ),
         )
         network.RecordSet(
             f"{self._name}_caa_record",
@@ -516,7 +532,9 @@ def __init__(
             resource_group_name=resource_group.name,
             ttl=30,
             zone_name=sre_dns_zone.name,
-            opts=child_opts,
+            opts=ResourceOptions.merge(
+                child_opts, ResourceOptions(parent=sre_dns_zone)
+            ),
         )
 
         # Define SRE internal DNS zone
@@ -525,10 +543,12 @@ def __init__(
             location="Global",
             private_zone_name=Output.concat("privatelink.", sre_fqdn),
             resource_group_name=resource_group.name,
-            opts=child_opts,
+            opts=ResourceOptions.merge(
+                child_opts, ResourceOptions(parent=sre_dns_zone)
+            ),
         )
         network.VirtualNetworkLink(
-            f"{self._name}_private_zone_vnet_link",
+            f"{self._name}_private_zone_internal_vnet_link",
             location="Global",
             private_zone_name=sre_private_dns_zone.name,
             registration_enabled=False,
@@ -537,7 +557,9 @@ def __init__(
             virtual_network_link_name=Output.concat(
                 "link-to-", sre_virtual_network.name
             ),
-            opts=child_opts,
+            opts=ResourceOptions.merge(
+                child_opts, ResourceOptions(parent=sre_virtual_network)
+            ),
         )
 
         # Register outputs
@@ -556,8 +578,8 @@ def __init__(
             resource_group_name=resource_group.name,
             virtual_network_name=sre_virtual_network.name,
         )
-        self.subnet_guacamole_database = network.get_subnet_output(
-            subnet_name=subnet_guacamole_database_name,
+        self.subnet_guacamole_containers_support = network.get_subnet_output(
+            subnet_name=subnet_guacamole_containers_support_name,
             resource_group_name=resource_group.name,
             virtual_network_name=sre_virtual_network.name,
         )
@@ -566,18 +588,18 @@ def __init__(
             resource_group_name=resource_group.name,
             virtual_network_name=sre_virtual_network.name,
         )
-        self.subnet_software_repositories = network.get_subnet_output(
-            subnet_name=subnet_software_repositories_name,
+        self.subnet_user_services_containers = network.get_subnet_output(
+            subnet_name=subnet_user_services_containers_name,
             resource_group_name=resource_group.name,
             virtual_network_name=sre_virtual_network.name,
         )
-        self.subnet_user_services_containers = network.get_subnet_output(
-            subnet_name=subnet_user_services_containers_name,
+        self.subnet_user_services_containers_support = network.get_subnet_output(
+            subnet_name=subnet_user_services_containers_support_name,
             resource_group_name=resource_group.name,
             virtual_network_name=sre_virtual_network.name,
         )
-        self.subnet_user_services_databases = network.get_subnet_output(
-            subnet_name=subnet_user_services_databases_name,
+        self.subnet_user_services_software_repositories = network.get_subnet_output(
+            subnet_name=subnet_user_services_software_repositories_name,
             resource_group_name=resource_group.name,
             virtual_network_name=sre_virtual_network.name,
         )
diff --git a/data_safe_haven/pulumi/components/sre_remote_desktop.py b/data_safe_haven/pulumi/components/sre_remote_desktop.py
index b475ca0616..879e32b15c 100644
--- a/data_safe_haven/pulumi/components/sre_remote_desktop.py
+++ b/data_safe_haven/pulumi/components/sre_remote_desktop.py
@@ -11,7 +11,7 @@
 )
 
 from data_safe_haven.external import AzureIPv4Range
-from data_safe_haven.pulumi.common.transformations import (
+from data_safe_haven.pulumi.common import (
     get_id_from_subnet,
     get_ip_address_from_container_group,
 )
@@ -49,7 +49,7 @@ def __init__(
         storage_account_name: Input[str],
         storage_account_resource_group_name: Input[str],
         subnet_guacamole_containers: Input[network.GetSubnetResult],
-        subnet_guacamole_database: Input[network.GetSubnetResult],
+        subnet_guacamole_containers_support: Input[network.GetSubnetResult],
         virtual_network: Input[network.VirtualNetwork],
         virtual_network_resource_group_name: Input[str],
         database_username: Input[str] | None = "postgresadmin",
@@ -86,11 +86,11 @@ def __init__(
             if s.address_prefix
             else []
         )
-        self.subnet_guacamole_database_id = Output.from_input(
-            subnet_guacamole_database
+        self.subnet_guacamole_containers_support_id = Output.from_input(
+            subnet_guacamole_containers_support
         ).apply(get_id_from_subnet)
-        self.subnet_guacamole_database_ip_addresses = Output.from_input(
-            subnet_guacamole_database
+        self.subnet_guacamole_containers_support_ip_addresses = Output.from_input(
+            subnet_guacamole_containers_support
         ).apply(
             lambda s: [
                 str(ip) for ip in AzureIPv4Range.from_cidr(s.address_prefix).available()
@@ -113,7 +113,7 @@ def __init__(
         opts: ResourceOptions | None = None,
     ) -> None:
         super().__init__("dsh:sre:RemoteDesktopComponent", name, {}, opts)
-        child_opts = ResourceOptions.merge(ResourceOptions(parent=self), opts)
+        child_opts = ResourceOptions.merge(opts, ResourceOptions(parent=self))
 
         # Deploy resource group
         resource_group = resources.ResourceGroup(
@@ -157,7 +157,7 @@ def __init__(
                 storage_account_key=props.storage_account_key,
                 storage_account_name=props.storage_account_name,
             ),
-            opts=child_opts,
+            opts=ResourceOptions.merge(child_opts, ResourceOptions(parent=file_share)),
         )
 
         # Define a PostgreSQL server
@@ -194,7 +194,9 @@ def __init__(
             f"{self._name}_connection_db_private_endpoint",
             custom_dns_configs=[
                 network.CustomDnsConfigPropertiesFormatArgs(
-                    ip_addresses=[props.subnet_guacamole_database_ip_addresses[0]],
+                    ip_addresses=[
+                        props.subnet_guacamole_containers_support_ip_addresses[0]
+                    ],
                 )
             ],
             private_endpoint_name=f"{stack_name}-endpoint-guacamole-db",
@@ -211,8 +213,10 @@ def __init__(
                 )
             ],
             resource_group_name=resource_group.name,
-            subnet=network.SubnetArgs(id=props.subnet_guacamole_database_id),
-            opts=child_opts,
+            subnet=network.SubnetArgs(id=props.subnet_guacamole_containers_support_id),
+            opts=ResourceOptions.merge(
+                child_opts, ResourceOptions(parent=connection_db_server)
+            ),
         )
         connection_db_name = "guacamole"
         connection_db = dbforpostgresql.Database(
@@ -221,7 +225,9 @@ def __init__(
             database_name=connection_db_name,
             resource_group_name=resource_group.name,
             server_name=connection_db_server.name,
-            opts=child_opts,
+            opts=ResourceOptions.merge(
+                child_opts, ResourceOptions(parent=connection_db_server)
+            ),
         )
 
         # Define a network profile
@@ -243,13 +249,13 @@ def __init__(
             network_profile_name=f"{stack_name}-np-guacamole",
             resource_group_name=props.virtual_network_resource_group_name,
             opts=ResourceOptions.merge(
+                child_opts,
                 ResourceOptions(
                     depends_on=[props.virtual_network],
                     ignore_changes=[
                         "container_network_interface_configurations"
                     ],  # allow container groups to be registered to this interface
                 ),
-                child_opts,
             ),
         )
 
@@ -321,7 +327,9 @@ def __init__(
                         ),
                         containerinstance.EnvironmentVariableArgs(
                             name="POSTGRES_HOSTNAME",
-                            value=props.subnet_guacamole_database_ip_addresses[0],
+                            value=props.subnet_guacamole_containers_support_ip_addresses[
+                                0
+                            ],
                         ),
                         containerinstance.EnvironmentVariableArgs(
                             name="POSTGRES_PASSWORD",
@@ -401,7 +409,9 @@ def __init__(
                         ),
                         containerinstance.EnvironmentVariableArgs(
                             name="POSTGRES_HOST",
-                            value=props.subnet_guacamole_database_ip_addresses[0],
+                            value=props.subnet_guacamole_containers_support_ip_addresses[
+                                0
+                            ],
                         ),
                         containerinstance.EnvironmentVariableArgs(
                             name="POSTGRES_PASSWORD",
@@ -451,10 +461,10 @@ def __init__(
                 ),
             ],
             opts=ResourceOptions.merge(
+                child_opts,
                 ResourceOptions(
                     delete_before_replace=True, replace_on_changes=["containers"]
                 ),
-                child_opts,
             ),
         )
 
diff --git a/data_safe_haven/pulumi/components/sre_software_repositories.py b/data_safe_haven/pulumi/components/sre_software_repositories.py
index 4d9df1d097..00fc4002c7 100644
--- a/data_safe_haven/pulumi/components/sre_software_repositories.py
+++ b/data_safe_haven/pulumi/components/sre_software_repositories.py
@@ -1,20 +1,17 @@
 """Pulumi component for SRE monitoring"""
 import pathlib
-from contextlib import suppress
 
 from pulumi import ComponentResource, Input, Output, ResourceOptions
-from pulumi_azure_native import containerinstance, network, resources, storage
+from pulumi_azure_native import containerinstance, network, storage
 
-from data_safe_haven.pulumi.common.transformations import (
-    get_available_ips_from_subnet,
-    get_id_from_subnet,
+from data_safe_haven.pulumi.common import (
     get_ip_address_from_container_group,
 )
 from data_safe_haven.pulumi.dynamic.file_share_file import (
     FileShareFile,
     FileShareFileProps,
 )
-from data_safe_haven.utility import FileReader
+from data_safe_haven.utility import FileReader, SoftwarePackageCategory
 
 
 class SRESoftwareRepositoriesProps:
@@ -25,31 +22,30 @@ def __init__(
         location: Input[str],
         networking_resource_group_name: Input[str],
         nexus_admin_password: Input[str],
-        software_packages: str,
+        resource_group_name: Input[str],
+        software_packages: SoftwarePackageCategory,
         sre_fqdn: Input[str],
         storage_account_key: Input[str],
         storage_account_name: Input[str],
         storage_account_resource_group_name: Input[str],
-        subnet: Input[network.GetSubnetResult],
+        subnet_id: Input[str],
         virtual_network: Input[network.VirtualNetwork],
         virtual_network_resource_group_name: Input[str],
     ) -> None:
         self.location = location
         self.networking_resource_group_name = networking_resource_group_name
         self.nexus_admin_password = Output.secret(nexus_admin_password)
-        self.nexus_packages: str | None = None
-        with suppress(KeyError):
-            self.nexus_packages = {"any": "all", "pre-approved": "selected"}[
-                software_packages
-            ]
+        self.nexus_packages: str | None = {
+            SoftwarePackageCategory.ANY: "all",
+            SoftwarePackageCategory.PRE_APPROVED: "selected",
+            SoftwarePackageCategory.NONE: None,
+        }[software_packages]
+        self.resource_group_name = resource_group_name
         self.sre_fqdn = sre_fqdn
         self.storage_account_key = storage_account_key
         self.storage_account_name = storage_account_name
         self.storage_account_resource_group_name = storage_account_resource_group_name
-        self.subnet_id = Output.from_input(subnet).apply(get_id_from_subnet)
-        self.subnet_ip_addresses = Output.from_input(subnet).apply(
-            get_available_ips_from_subnet
-        )
+        self.subnet_id = subnet_id
         self.virtual_network = virtual_network
         self.virtual_network_resource_group_name = virtual_network_resource_group_name
 
@@ -65,15 +61,7 @@ def __init__(
         opts: ResourceOptions | None = None,
     ) -> None:
         super().__init__("dsh:sre:SRESoftwareRepositoriesComponent", name, {}, opts)
-        child_opts = ResourceOptions.merge(ResourceOptions(parent=self), opts)
-
-        # Deploy resource group
-        resource_group = resources.ResourceGroup(
-            f"{self._name}_resource_group",
-            location=props.location,
-            resource_group_name=f"{stack_name}-rg-software-repositories",
-            opts=child_opts,
-        )
+        child_opts = ResourceOptions.merge(opts, ResourceOptions(parent=self))
 
         # Define configuration file shares
         file_share_caddy = storage.FileShare(
@@ -118,7 +106,9 @@ def __init__(
                 storage_account_key=props.storage_account_key,
                 storage_account_name=props.storage_account_name,
             ),
-            opts=child_opts,
+            opts=ResourceOptions.merge(
+                child_opts, ResourceOptions(parent=file_share_caddy)
+            ),
         )
 
         # Upload Nexus allowlists
@@ -134,7 +124,9 @@ def __init__(
                 storage_account_key=props.storage_account_key,
                 storage_account_name=props.storage_account_name,
             ),
-            opts=child_opts,
+            opts=ResourceOptions.merge(
+                child_opts, ResourceOptions(parent=file_share_nexus)
+            ),
         )
         pypi_reader = FileReader(
             resources_path / "software_repositories" / "allowlists" / "pypi.allowlist"
@@ -148,7 +140,9 @@ def __init__(
                 storage_account_key=props.storage_account_key,
                 storage_account_name=props.storage_account_name,
             ),
-            opts=child_opts,
+            opts=ResourceOptions.merge(
+                child_opts, ResourceOptions(parent=file_share_nexus)
+            ),
         )
 
         # Define a network profile
@@ -170,13 +164,13 @@ def __init__(
             network_profile_name=f"{stack_name}-np-software-repositories",
             resource_group_name=props.virtual_network_resource_group_name,
             opts=ResourceOptions.merge(
+                child_opts,
                 ResourceOptions(
                     depends_on=[props.virtual_network],
                     ignore_changes=[
                         "container_network_interface_configurations"
                     ],  # allow container groups to be registered to this interface
                 ),
-                child_opts,
             ),
         )
 
@@ -283,7 +277,7 @@ def __init__(
                     id=container_network_profile.id,
                 ),
                 os_type=containerinstance.OperatingSystemTypes.LINUX,
-                resource_group_name=resource_group.name,
+                resource_group_name=props.resource_group_name,
                 restart_policy=containerinstance.ContainerGroupRestartPolicy.ALWAYS,
                 sku=containerinstance.ContainerGroupSku.STANDARD,
                 volumes=[
@@ -313,14 +307,14 @@ def __init__(
                     ),
                 ],
                 opts=ResourceOptions.merge(
+                    child_opts,
                     ResourceOptions(
                         delete_before_replace=True, replace_on_changes=["containers"]
                     ),
-                    child_opts,
                 ),
             )
             # Register the container group in the SRE private DNS zone
-            network.PrivateRecordSet(
+            private_dns_record_set = network.PrivateRecordSet(
                 f"{self._name}_nexus_private_record_set",
                 a_records=[
                     network.ARecordArgs(
@@ -334,7 +328,9 @@ def __init__(
                 relative_record_set_name="nexus",
                 resource_group_name=props.networking_resource_group_name,
                 ttl=3600,
-                opts=child_opts,
+                opts=ResourceOptions.merge(
+                    child_opts, ResourceOptions(parent=container_group)
+                ),
             )
             # Redirect the public DNS to private DNS
             network.RecordSet(
@@ -347,5 +343,7 @@ def __init__(
                 resource_group_name=props.networking_resource_group_name,
                 ttl=3600,
                 zone_name=props.sre_fqdn,
-                opts=child_opts,
+                opts=ResourceOptions.merge(
+                    child_opts, ResourceOptions(parent=private_dns_record_set)
+                ),
             )
diff --git a/data_safe_haven/pulumi/components/sre_user_services.py b/data_safe_haven/pulumi/components/sre_user_services.py
index c611c167ac..cb67408111 100644
--- a/data_safe_haven/pulumi/components/sre_user_services.py
+++ b/data_safe_haven/pulumi/components/sre_user_services.py
@@ -1,10 +1,15 @@
 from pulumi import ComponentResource, Input, Output, ResourceOptions
 from pulumi_azure_native import network, resources
 
-from data_safe_haven.pulumi.common.transformations import get_id_from_subnet
+from data_safe_haven.pulumi.common import get_id_from_subnet
+from data_safe_haven.utility import SoftwarePackageCategory
 
 from .sre_gitea_server import SREGiteaServerComponent, SREGiteaServerProps
 from .sre_hedgedoc_server import SREHedgeDocServerComponent, SREHedgeDocServerProps
+from .sre_software_repositories import (
+    SRESoftwareRepositoriesComponent,
+    SRESoftwareRepositoriesProps,
+)
 
 
 class SREUserServicesProps:
@@ -23,13 +28,16 @@ def __init__(
         ldap_user_security_group_name: Input[str],
         location: Input[str],
         networking_resource_group_name: Input[str],
+        nexus_admin_password: Input[str],
+        software_packages: SoftwarePackageCategory,
         sre_fqdn: Input[str],
         sre_private_dns_zone_id: Input[str],
         storage_account_key: Input[str],
         storage_account_name: Input[str],
         storage_account_resource_group_name: Input[str],
         subnet_containers: Input[network.GetSubnetResult],
-        subnet_databases: Input[network.GetSubnetResult],
+        subnet_containers_support: Input[network.GetSubnetResult],
+        subnet_software_repositories: Input[network.GetSubnetResult],
         virtual_network: Input[network.VirtualNetwork],
         virtual_network_resource_group_name: Input[str],
     ) -> None:
@@ -44,6 +52,8 @@ def __init__(
         self.ldap_user_security_group_name = ldap_user_security_group_name
         self.location = location
         self.networking_resource_group_name = networking_resource_group_name
+        self.nexus_admin_password = Output.secret(nexus_admin_password)
+        self.software_packages = software_packages
         self.sre_fqdn = sre_fqdn
         self.sre_private_dns_zone_id = sre_private_dns_zone_id
         self.storage_account_key = storage_account_key
@@ -52,9 +62,12 @@ def __init__(
         self.subnet_containers_id = Output.from_input(subnet_containers).apply(
             get_id_from_subnet
         )
-        self.subnet_databases_id = Output.from_input(subnet_databases).apply(
-            get_id_from_subnet
-        )
+        self.subnet_containers_support_id = Output.from_input(
+            subnet_containers_support
+        ).apply(get_id_from_subnet)
+        self.subnet_software_repositories_id = Output.from_input(
+            subnet_software_repositories
+        ).apply(get_id_from_subnet)
         self.virtual_network = virtual_network
         self.virtual_network_resource_group_name = virtual_network_resource_group_name
 
@@ -70,7 +83,7 @@ def __init__(
         opts: ResourceOptions | None = None,
     ) -> None:
         super().__init__("dsh:sre:UserServicesComponent", name, {}, opts)
-        child_opts = ResourceOptions.merge(ResourceOptions(parent=self), opts)
+        child_opts = ResourceOptions.merge(opts, ResourceOptions(parent=self))
 
         # Deploy resource group
         resource_group = resources.ResourceGroup(
@@ -99,13 +112,13 @@ def __init__(
             network_profile_name=f"{stack_name}-np-user-services",
             resource_group_name=props.virtual_network_resource_group_name,
             opts=ResourceOptions.merge(
+                child_opts,
                 ResourceOptions(
                     depends_on=[props.virtual_network],
                     ignore_changes=[
                         "container_network_interface_configurations"
                     ],  # allow container groups to be registered to this interface
                 ),
-                child_opts,
             ),
         )
 
@@ -114,7 +127,7 @@ def __init__(
             "sre_gitea_server",
             stack_name,
             SREGiteaServerProps(
-                database_subnet_id=props.subnet_databases_id,
+                database_subnet_id=props.subnet_containers_support_id,
                 database_password=props.gitea_database_password,
                 ldap_bind_dn=props.ldap_bind_dn,
                 ldap_root_dn=props.ldap_root_dn,
@@ -142,7 +155,7 @@ def __init__(
             "sre_hedgedoc_server",
             stack_name,
             SREHedgeDocServerProps(
-                database_subnet_id=props.subnet_databases_id,
+                database_subnet_id=props.subnet_containers_support_id,
                 database_password=props.hedgedoc_database_password,
                 domain_netbios_name=props.domain_netbios_name,
                 ldap_bind_dn=props.ldap_bind_dn,
@@ -165,3 +178,24 @@ def __init__(
             ),
             opts=child_opts,
         )
+
+        # Deploy software repository servers
+        SRESoftwareRepositoriesComponent(
+            "sre_software_repositories",
+            stack_name,
+            SRESoftwareRepositoriesProps(
+                location=props.location,
+                networking_resource_group_name=props.networking_resource_group_name,
+                nexus_admin_password=props.nexus_admin_password,
+                resource_group_name=resource_group.name,
+                sre_fqdn=props.sre_fqdn,
+                software_packages=props.software_packages,
+                storage_account_key=props.storage_account_key,
+                storage_account_name=props.storage_account_name,
+                storage_account_resource_group_name=props.storage_account_resource_group_name,
+                subnet_id=props.subnet_software_repositories_id,
+                virtual_network=props.virtual_network,
+                virtual_network_resource_group_name=props.virtual_network_resource_group_name,
+            ),
+            opts=child_opts,
+        )
diff --git a/data_safe_haven/pulumi/components/sre_workspace.py b/data_safe_haven/pulumi/components/sre_workspace.py
index 98d8486bed..f1774add9f 100644
--- a/data_safe_haven/pulumi/components/sre_workspace.py
+++ b/data_safe_haven/pulumi/components/sre_workspace.py
@@ -7,7 +7,7 @@
 
 from data_safe_haven.exceptions import DataSafeHavenPulumiError
 from data_safe_haven.functions import b64encode, replace_separators
-from data_safe_haven.pulumi.common.transformations import (
+from data_safe_haven.pulumi.common import (
     get_available_ips_from_subnet,
     get_name_from_rg,
     get_name_from_subnet,
@@ -97,7 +97,7 @@ def __init__(
         opts: ResourceOptions | None = None,
     ) -> None:
         super().__init__("dsh:sre:WorkspaceComponent", name, {}, opts)
-        child_opts = ResourceOptions.merge(ResourceOptions(parent=self), opts)
+        child_opts = ResourceOptions.merge(opts, ResourceOptions(parent=self))
 
         # Deploy resource group
         resource_group = resources.ResourceGroup(
diff --git a/data_safe_haven/pulumi/components/virtual_machine.py b/data_safe_haven/pulumi/components/virtual_machine.py
index fdfbe9dd72..5d7b7ca939 100644
--- a/data_safe_haven/pulumi/components/virtual_machine.py
+++ b/data_safe_haven/pulumi/components/virtual_machine.py
@@ -127,7 +127,7 @@ class VMComponent(ComponentResource):
 
     def __init__(self, name: str, props: VMProps, opts: ResourceOptions | None = None):
         super().__init__("dsh:common:VMComponent", name, {}, opts)
-        child_opts = ResourceOptions.merge(ResourceOptions(parent=self), opts)
+        child_opts = ResourceOptions.merge(opts, ResourceOptions(parent=self))
 
         # Retrieve existing resources
         subnet = network.get_subnet_output(
@@ -207,10 +207,10 @@ def __init__(self, name: str, props: VMProps, opts: ResourceOptions | None = Non
             ),
             vm_name=props.vm_name,
             opts=ResourceOptions.merge(
+                child_opts,
                 ResourceOptions(
                     delete_before_replace=True, replace_on_changes=["os_profile"]
                 ),
-                child_opts,
             ),
         )
 
@@ -233,7 +233,9 @@ def __init__(self, name: str, props: VMProps, opts: ResourceOptions | None = Non
                 type_handler_version=props.log_analytics_extension_version,
                 vm_extension_name=props.log_analytics_extension_name,
                 vm_name=virtual_machine.name,
-                opts=child_opts,
+                opts=ResourceOptions.merge(
+                    child_opts, ResourceOptions(parent=virtual_machine)
+                ),
             )
 
         # Register outputs
diff --git a/data_safe_haven/pulumi/declarative_sre.py b/data_safe_haven/pulumi/declarative_sre.py
index ef8c073879..eb2e969d85 100644
--- a/data_safe_haven/pulumi/declarative_sre.py
+++ b/data_safe_haven/pulumi/declarative_sre.py
@@ -14,10 +14,6 @@
     SRERemoteDesktopComponent,
     SRERemoteDesktopProps,
 )
-from .components.sre_software_repositories import (
-    SRESoftwareRepositoriesComponent,
-    SRESoftwareRepositoriesProps,
-)
 from .components.sre_user_services import SREUserServicesComponent, SREUserServicesProps
 from .components.sre_workspace import (
     SREWorkspaceComponent,
@@ -163,7 +159,7 @@ def run(self) -> None:
                 ldap_user_security_group_name=ldap_user_security_group_name,
                 location=self.cfg.azure.location,
                 subnet_guacamole_containers=networking.subnet_guacamole_containers,
-                subnet_guacamole_database=networking.subnet_guacamole_database,
+                subnet_guacamole_containers_support=networking.subnet_guacamole_containers_support,
                 storage_account_key=data.storage_account_state_key,
                 storage_account_name=data.storage_account_state_name,
                 storage_account_resource_group_name=data.resource_group_name,
@@ -209,25 +205,6 @@ def run(self) -> None:
             ),
         )
 
-        # Deploy software repository servers
-        SRESoftwareRepositoriesComponent(
-            "shm_update_servers",
-            self.stack_name,
-            SRESoftwareRepositoriesProps(
-                location=self.cfg.azure.location,
-                networking_resource_group_name=networking.resource_group.name,
-                nexus_admin_password=data.password_nexus_admin,
-                sre_fqdn=networking.sre_fqdn,
-                software_packages=self.cfg.sres[self.sre_name].software_packages,
-                storage_account_key=data.storage_account_state_key,
-                storage_account_name=data.storage_account_state_name,
-                storage_account_resource_group_name=data.resource_group_name,
-                subnet=networking.subnet_software_repositories,
-                virtual_network=networking.virtual_network,
-                virtual_network_resource_group_name=networking.resource_group.name,
-            ),
-        )
-
         # Deploy containerised user services
         SREUserServicesComponent(
             "sre_user_services",
@@ -246,13 +223,16 @@ def run(self) -> None:
                 ldap_user_search_base=ldap_user_search_base,
                 location=self.cfg.azure.location,
                 networking_resource_group_name=networking.resource_group.name,
+                nexus_admin_password=data.password_nexus_admin,
+                software_packages=self.cfg.sres[self.sre_name].software_packages,
                 sre_fqdn=networking.sre_fqdn,
                 sre_private_dns_zone_id=networking.sre_private_dns_zone_id,
                 storage_account_key=data.storage_account_state_key,
                 storage_account_name=data.storage_account_state_name,
                 storage_account_resource_group_name=data.resource_group_name,
                 subnet_containers=networking.subnet_user_services_containers,
-                subnet_databases=networking.subnet_user_services_databases,
+                subnet_containers_support=networking.subnet_user_services_containers_support,
+                subnet_software_repositories=networking.subnet_user_services_software_repositories,
                 virtual_network=networking.virtual_network,
                 virtual_network_resource_group_name=networking.resource_group.name,
             ),