Repository: Azure/ARO-RP
Pull request #1369 (2021-03-08 16:29:52)

Gateway preparation 1
This PR is a set of renames and code moves which have **no effect** on execution but help prepare to build out the gateway component.



Patch:
diff --git a/cmd/aro/deploy.go b/cmd/aro/deploy.go
index 26c7a7b61..8275042de 100644
--- a/cmd/aro/deploy.go
+++ b/cmd/aro/deploy.go
@@ -65,10 +65,17 @@ func deploy(ctx context.Context, log *logrus.Entry) error {
 		return err
 	}
 
-	err = deployer.Deploy(ctx)
+	err = deployer.DeployRP(ctx)
 	if err != nil {
 		return err
 	}
 
-	return deployer.Upgrade(ctx)
+	err = deployer.UpgradeRP(ctx)
+	if err != nil {
+		return err
+	}
+
+	// Must be last step so we can be sure there are no RPs at older versions
+	// still serving
+	return deployer.SaveVersion(ctx)
 }
diff --git a/pkg/api/admin/openshiftcluster.go b/pkg/api/admin/openshiftcluster.go
index 296cc3f79..49451ea1c 100644
--- a/pkg/api/admin/openshiftcluster.go
+++ b/pkg/api/admin/openshiftcluster.go
@@ -85,7 +85,7 @@ type NetworkProfile struct {
 	PodCIDR     string `json:"podCidr,omitempty"`
 	ServiceCIDR string `json:"serviceCidr,omitempty"`
 
-	PrivateEndpointIP string `json:"privateEndpointIp,omitempty"`
+	APIServerPrivateEndpointIP string `json:"privateEndpointIp,omitempty"`
 }
 
 // MasterProfile represents a master profile.
diff --git a/pkg/api/admin/openshiftcluster_convert.go b/pkg/api/admin/openshiftcluster_convert.go
index df3bfe307..0140a280c 100644
--- a/pkg/api/admin/openshiftcluster_convert.go
+++ b/pkg/api/admin/openshiftcluster_convert.go
@@ -41,9 +41,9 @@ func (c *openShiftClusterConverter) ToExternal(oc *api.OpenShiftCluster) interfa
 				SPObjectID: oc.Properties.ServicePrincipalProfile.SPObjectID,
 			},
 			NetworkProfile: NetworkProfile{
-				PodCIDR:           oc.Properties.NetworkProfile.PodCIDR,
-				ServiceCIDR:       oc.Properties.NetworkProfile.ServiceCIDR,
-				PrivateEndpointIP: oc.Properties.NetworkProfile.PrivateEndpointIP,
+				PodCIDR:                    oc.Properties.NetworkProfile.PodCIDR,
+				ServiceCIDR:                oc.Properties.NetworkProfile.ServiceCIDR,
+				APIServerPrivateEndpointIP: oc.Properties.NetworkProfile.APIServerPrivateEndpointIP,
 			},
 			MasterProfile: MasterProfile{
 				VMSize:   VMSize(oc.Properties.MasterProfile.VMSize),
@@ -156,7 +156,7 @@ func (c *openShiftClusterConverter) ToInternal(_oc interface{}, out *api.OpenShi
 	out.Properties.ServicePrincipalProfile.SPObjectID = oc.Properties.ServicePrincipalProfile.SPObjectID
 	out.Properties.NetworkProfile.PodCIDR = oc.Properties.NetworkProfile.PodCIDR
 	out.Properties.NetworkProfile.ServiceCIDR = oc.Properties.NetworkProfile.ServiceCIDR
-	out.Properties.NetworkProfile.PrivateEndpointIP = oc.Properties.NetworkProfile.PrivateEndpointIP
+	out.Properties.NetworkProfile.APIServerPrivateEndpointIP = oc.Properties.NetworkProfile.APIServerPrivateEndpointIP
 	out.Properties.MasterProfile.VMSize = api.VMSize(oc.Properties.MasterProfile.VMSize)
 	out.Properties.MasterProfile.SubnetID = oc.Properties.MasterProfile.SubnetID
 	out.Properties.StorageSuffix = oc.Properties.StorageSuffix
diff --git a/pkg/api/openshiftcluster.go b/pkg/api/openshiftcluster.go
index 52a12bc22..b42b6aacd 100644
--- a/pkg/api/openshiftcluster.go
+++ b/pkg/api/openshiftcluster.go
@@ -166,7 +166,7 @@ type NetworkProfile struct {
 	PodCIDR     string `json:"podCidr,omitempty"`
 	ServiceCIDR string `json:"serviceCidr,omitempty"`
 
-	PrivateEndpointIP string `json:"privateEndpointIp,omitempty"`
+	APIServerPrivateEndpointIP string `json:"privateEndpointIp,omitempty"`
 }
 
 // MasterProfile represents a master profile
diff --git a/pkg/cluster/install.go b/pkg/cluster/install.go
index 5989e403e..f2ec6f7c4 100644
--- a/pkg/cluster/install.go
+++ b/pkg/cluster/install.go
@@ -108,7 +108,7 @@ func (m *manager) Install(ctx context.Context) error {
 			steps.AuthorizationRefreshingAction(m.fpAuthorizer, steps.Action(m.attachNSGsAndPatch)),
 			steps.Action(m.ensureBillingRecord),
 			steps.AuthorizationRefreshingAction(m.fpAuthorizer, steps.Action(m.deployResourceTemplate)),
-			steps.Action(m.createPrivateEndpoint),
+			steps.Action(m.createAPIServerPrivateEndpoint),
 			steps.Action(m.createCertificates),
 			steps.Action(m.initializeKubernetesClients),
 			steps.Condition(m.bootstrapConfigMapReady, 30*time.Minute),
diff --git a/pkg/cluster/ipaddresses.go b/pkg/cluster/ipaddresses.go
index 4c093de53..444d7e187 100644
--- a/pkg/cluster/ipaddresses.go
+++ b/pkg/cluster/ipaddresses.go
@@ -189,19 +189,19 @@ func (m *manager) updateAPIIPEarly(ctx context.Context) error {
 	return err
 }
 
-func (m *manager) createPrivateEndpoint(ctx context.Context) error {
+func (m *manager) createAPIServerPrivateEndpoint(ctx context.Context) error {
 	err := m.privateendpoint.Create(ctx, m.doc)
 	if err != nil {
 		return err
 	}
 
-	privateEndpointIP, err := m.privateendpoint.GetIP(ctx, m.doc)
+	apiServerPrivateEndpointIP, err := m.privateendpoint.GetIP(ctx, m.doc)
 	if err != nil {
 		return err
 	}
 
 	m.doc, err = m.db.PatchWithLease(ctx, m.doc.Key, func(doc *api.OpenShiftClusterDocument) error {
-		doc.OpenShiftCluster.Properties.NetworkProfile.PrivateEndpointIP = privateEndpointIP
+		doc.OpenShiftCluster.Properties.NetworkProfile.APIServerPrivateEndpointIP = apiServerPrivateEndpointIP
 		return nil
 	})
 	return err
diff --git a/pkg/deploy/config.go b/pkg/deploy/config.go
index b6f63493f..0c125a280 100644
--- a/pkg/deploy/config.go
+++ b/pkg/deploy/config.go
@@ -20,10 +20,10 @@ type Config struct {
 
 // RPConfig represents individual RP configuration
 type RPConfig struct {
-	Location          string         `json:"location,omitempty"`
-	SubscriptionID    string         `json:"subscriptionId,omitempty"`
-	ResourceGroupName string         `json:"resourceGroupName,omitempty"`
-	Configuration     *Configuration `json:"configuration,omitempty"`
+	Location            string         `json:"location,omitempty"`
+	SubscriptionID      string         `json:"subscriptionId,omitempty"`
+	RPResourceGroupName string         `json:"resourceGroupName,omitempty"`
+	Configuration       *Configuration `json:"configuration,omitempty"`
 }
 
 // Configuration represents configuration structure
diff --git a/pkg/deploy/deploy.go b/pkg/deploy/deploy.go
index 6a9fd18c5..2c338487a 100644
--- a/pkg/deploy/deploy.go
+++ b/pkg/deploy/deploy.go
@@ -5,19 +5,12 @@ package deploy
 
 import (
 	"context"
-	"encoding/base64"
-	"encoding/json"
 	"reflect"
 	"strings"
 
-	mgmtdns "github.com/Azure/azure-sdk-for-go/services/dns/mgmt/2018-05-01/dns"
-	mgmtfeatures "github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2019-07-01/features"
-	"github.com/Azure/go-autorest/autorest/azure"
 	"github.com/Azure/go-autorest/autorest/azure/auth"
-	"github.com/Azure/go-autorest/autorest/to"
 	"github.com/sirupsen/logrus"
 
-	"github.com/Azure/ARO-RP/pkg/deploy/generator"
 	"github.com/Azure/ARO-RP/pkg/env"
 	"github.com/Azure/ARO-RP/pkg/util/arm"
 	"github.com/Azure/ARO-RP/pkg/util/azureclient/mgmt/compute"
@@ -33,8 +26,9 @@ var _ Deployer = (*deployer)(nil)
 
 type Deployer interface {
 	PreDeploy(context.Context) error
-	Deploy(context.Context) error
-	Upgrade(context.Context) error
+	DeployRP(context.Context) error
+	UpgradeRP(context.Context) error
+	SaveVersion(context.Context) error
 }
 
 type deployer struct {
@@ -105,143 +99,6 @@ func New(ctx context.Context, log *logrus.Entry, env env.Core, config *RPConfig,
 	}, nil
 }
 
-func (d *deployer) Deploy(ctx context.Context) error {
-	encryptionAtHostSupported, err := d.encryptionAtHostSupported(ctx)
-	if err != nil {
-		return err
-	}
-	if !encryptionAtHostSupported {
-		d.log.Warn("encryption at host not supported")
-	}
-
-	msi, err := d.userassignedidentities.Get(ctx, d.config.ResourceGroupName, "aro-rp-"+d.config.Location)
-	if err != nil {
-		return err
-	}
-
-	deploymentName := "rp-production-" + d.version
-
-	b, err := Asset(generator.FileRPProduction)
-	if err != nil {
-		return err
-	}
-
-	var template map[string]interface{}
-	err = json.Unmarshal(b, &template)
-	if err != nil {
-		return err
-	}
-
-	parameters := d.getParameters(template["parameters"].(map[string]interface{}))
-	parameters.Parameters["adminApiCaBundle"] = &arm.ParametersParameter{
-		Value: base64.StdEncoding.EncodeToString([]byte(*d.config.Configuration.AdminAPICABundle)),
-	}
-	parameters.Parameters["encryptionAtHost"] = &arm.ParametersParameter{
-		Value: encryptionAtHostSupported,
-	}
-	parameters.Parameters["extraCosmosDBIPs"] = &arm.ParametersParameter{
-		Value: strings.Join(d.config.Configuration.ExtraCosmosDBIPs, ","),
-	}
-	parameters.Parameters["rpImage"] = &arm.ParametersParameter{
-		Value: *d.config.Configuration.RPImagePrefix + ":" + d.version,
-	}
-	parameters.Parameters["rpServicePrincipalId"] = &arm.ParametersParameter{
-		Value: msi.PrincipalID.String(),
-	}
-	parameters.Parameters["vmssName"] = &arm.ParametersParameter{
-		Value: d.version,
-	}
-
-	for i := 0; i < 2; i++ {
-		d.log.Printf("deploying %s", deploymentName)
-		err = d.deployments.CreateOrUpdateAndWait(ctx, d.config.ResourceGroupName, deploymentName, mgmtfeatures.Deployment{
-			Properties: &mgmtfeatures.DeploymentProperties{
-				Template:   template,
-				Mode:       mgmtfeatures.Incremental,
-				Parameters: parameters.Parameters,
-			},
-		})
-		if serviceErr, ok := err.(*azure.ServiceError); ok &&
-			serviceErr.Code == "DeploymentFailed" &&
-			i < 1 {
-			// on new RP deployments, we get a spurious DeploymentFailed error
-			// from the Microsoft.Insights/metricAlerts resources indicating
-			// that rp-lb can't be found, even though it exists and the
-			// resources correctly have a dependsOn stanza referring to it.
-			// Retry once.
-			d.log.Print(err)
-			continue
-		}
-		if err != nil {
-			return err
-		}
-
-		break
-	}
-
-	return d.configureDNS(ctx)
-}
-
-func (d *deployer) configureDNS(ctx context.Context) error {
-	rpPip, err := d.publicipaddresses.Get(ctx, d.config.ResourceGroupName, "rp-pip", "")
-	if err != nil {
-		return err
-	}
-
-	portalPip, err := d.publicipaddresses.Get(ctx, d.config.ResourceGroupName, "portal-pip", "")
-	if err != nil {
-		return err
-	}
-
-	zone, err := d.zones.Get(ctx, d.config.ResourceGroupName, d.config.Location+"."+*d.config.Configuration.ClusterParentDomainName)
-	if err != nil {
-		return err
-	}
-
-	_, err = d.globalrecordsets.CreateOrUpdate(ctx, *d.config.Configuration.GlobalResourceGroupName, *d.config.Configuration.RPParentDomainName, "rp."+d.config.Location, mgmtdns.A, mgmtdns.RecordSet{
-		RecordSetProperties: &mgmtdns.RecordSetProperties{
-			TTL: to.Int64Ptr(3600),
-			ARecords: &[]mgmtdns.ARecord{
-				{
-					Ipv4Address: rpPip.IPAddress,
-				},
-			},
-		},
-	}, "", "")
-	if err != nil {
-		return err
-	}
-
-	_, err = d.globalrecordsets.CreateOrUpdate(ctx, *d.config.Configuration.GlobalResourceGroupName, *d.config.Configuration.RPParentDomainName, d.config.Location+".admin", mgmtdns.A, mgmtdns.RecordSet{
-		RecordSetProperties: &mgmtdns.RecordSetProperties{
-			TTL: to.Int64Ptr(3600),
-			ARecords: &[]mgmtdns.ARecord{
-				{
-					Ipv4Address: portalPip.IPAddress,
-				},
-			},
-		},
-	}, "", "")
-	if err != nil {
-		return err
-	}
-
-	nsRecords := make([]mgmtdns.NsRecord, 0, len(*zone.NameServers))
-	for i := range *zone.NameServers {
-		nsRecords = append(nsRecords, mgmtdns.NsRecord{
-			Nsdname: &(*zone.NameServers)[i],
-		})
-	}
-
-	_, err = d.globalrecordsets.CreateOrUpdate(ctx, *d.config.Configuration.GlobalResourceGroupName, *d.config.Configuration.ClusterParentDomainName, d.config.Location, mgmtdns.NS, mgmtdns.RecordSet{
-		RecordSetProperties: &mgmtdns.RecordSetProperties{
-			TTL:       to.Int64Ptr(3600),
-			NsRecords: &nsRecords,
-		},
-	}, "", "")
-	return err
-}
-
 // getParameters returns an *arm.Parameters populated with parameter names and
 // values.  The names are taken from the ps argument and the values are taken
 // from d.config.Configuration.
diff --git a/pkg/deploy/deploy_rp.go b/pkg/deploy/deploy_rp.go
new file mode 100644
index 000000000..6a881e12d
--- /dev/null
+++ b/pkg/deploy/deploy_rp.go
@@ -0,0 +1,156 @@
+package deploy
+
+// Copyright (c) Microsoft Corporation.
+// Licensed under the Apache License 2.0.
+
+import (
+	"context"
+	"encoding/base64"
+	"encoding/json"
+	"strings"
+
+	mgmtdns "github.com/Azure/azure-sdk-for-go/services/dns/mgmt/2018-05-01/dns"
+	mgmtfeatures "github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2019-07-01/features"
+	"github.com/Azure/go-autorest/autorest/azure"
+	"github.com/Azure/go-autorest/autorest/to"
+
+	"github.com/Azure/ARO-RP/pkg/deploy/generator"
+	"github.com/Azure/ARO-RP/pkg/util/arm"
+)
+
+func (d *deployer) DeployRP(ctx context.Context) error {
+	encryptionAtHostSupported, err := d.encryptionAtHostSupported(ctx)
+	if err != nil {
+		return err
+	}
+	if !encryptionAtHostSupported {
+		d.log.Warn("encryption at host not supported")
+	}
+
+	rpMSI, err := d.userassignedidentities.Get(ctx, d.config.RPResourceGroupName, "aro-rp-"+d.config.Location)
+	if err != nil {
+		return err
+	}
+
+	deploymentName := "rp-production-" + d.version
+
+	b, err := Asset(generator.FileRPProduction)
+	if err != nil {
+		return err
+	}
+
+	var template map[string]interface{}
+	err = json.Unmarshal(b, &template)
+	if err != nil {
+		return err
+	}
+
+	parameters := d.getParameters(template["parameters"].(map[string]interface{}))
+	parameters.Parameters["adminApiCaBundle"] = &arm.ParametersParameter{
+		Value: base64.StdEncoding.EncodeToString([]byte(*d.config.Configuration.AdminAPICABundle)),
+	}
+	parameters.Parameters["encryptionAtHost"] = &arm.ParametersParameter{
+		Value: encryptionAtHostSupported,
+	}
+	parameters.Parameters["extraCosmosDBIPs"] = &arm.ParametersParameter{
+		Value: strings.Join(d.config.Configuration.ExtraCosmosDBIPs, ","),
+	}
+	parameters.Parameters["rpImage"] = &arm.ParametersParameter{
+		Value: *d.config.Configuration.RPImagePrefix + ":" + d.version,
+	}
+	parameters.Parameters["rpServicePrincipalId"] = &arm.ParametersParameter{
+		Value: rpMSI.PrincipalID.String(),
+	}
+	parameters.Parameters["vmssName"] = &arm.ParametersParameter{
+		Value: d.version,
+	}
+
+	for i := 0; i < 2; i++ {
+		d.log.Printf("deploying %s", deploymentName)
+		err = d.deployments.CreateOrUpdateAndWait(ctx, d.config.RPResourceGroupName, deploymentName, mgmtfeatures.Deployment{
+			Properties: &mgmtfeatures.DeploymentProperties{
+				Template:   template,
+				Mode:       mgmtfeatures.Incremental,
+				Parameters: parameters.Parameters,
+			},
+		})
+		if serviceErr, ok := err.(*azure.ServiceError); ok &&
+			serviceErr.Code == "DeploymentFailed" &&
+			i < 1 {
+			// on new RP deployments, we get a spurious DeploymentFailed error
+			// from the Microsoft.Insights/metricAlerts resources indicating
+			// that rp-lb can't be found, even though it exists and the
+			// resources correctly have a dependsOn stanza referring to it.
+			// Retry once.
+			d.log.Print(err)
+			continue
+		}
+		if err != nil {
+			return err
+		}
+
+		break
+	}
+
+	return d.configureDNS(ctx)
+}
+
+func (d *deployer) configureDNS(ctx context.Context) error {
+	rpPip, err := d.publicipaddresses.Get(ctx, d.config.RPResourceGroupName, "rp-pip", "")
+	if err != nil {
+		return err
+	}
+
+	portalPip, err := d.publicipaddresses.Get(ctx, d.config.RPResourceGroupName, "portal-pip", "")
+	if err != nil {
+		return err
+	}
+
+	zone, err := d.zones.Get(ctx, d.config.RPResourceGroupName, d.config.Location+"."+*d.config.Configuration.ClusterParentDomainName)
+	if err != nil {
+		return err
+	}
+
+	_, err = d.globalrecordsets.CreateOrUpdate(ctx, *d.config.Configuration.GlobalResourceGroupName, *d.config.Configuration.RPParentDomainName, "rp."+d.config.Location, mgmtdns.A, mgmtdns.RecordSet{
+		RecordSetProperties: &mgmtdns.RecordSetProperties{
+			TTL: to.Int64Ptr(3600),
+			ARecords: &[]mgmtdns.ARecord{
+				{
+					Ipv4Address: rpPip.IPAddress,
+				},
+			},
+		},
+	}, "", "")
+	if err != nil {
+		return err
+	}
+
+	_, err = d.globalrecordsets.CreateOrUpdate(ctx, *d.config.Configuration.GlobalResourceGroupName, *d.config.Configuration.RPParentDomainName, d.config.Location+".admin", mgmtdns.A, mgmtdns.RecordSet{
+		RecordSetProperties: &mgmtdns.RecordSetProperties{
+			TTL: to.Int64Ptr(3600),
+			ARecords: &[]mgmtdns.ARecord{
+				{
+					Ipv4Address: portalPip.IPAddress,
+				},
+			},
+		},
+	}, "", "")
+	if err != nil {
+		return err
+	}
+
+	nsRecords := make([]mgmtdns.NsRecord, 0, len(*zone.NameServers))
+	for i := range *zone.NameServers {
+		nsRecords = append(nsRecords, mgmtdns.NsRecord{
+			Nsdname: &(*zone.NameServers)[i],
+		})
+	}
+
+	_, err = d.globalrecordsets.CreateOrUpdate(ctx, *d.config.Configuration.GlobalResourceGroupName, *d.config.Configuration.ClusterParentDomainName, d.config.Location, mgmtdns.NS, mgmtdns.RecordSet{
+		RecordSetProperties: &mgmtdns.RecordSetProperties{
+			TTL:       to.Int64Ptr(3600),
+			NsRecords: &nsRecords,
+		},
+	}, "", "")
+	return err
+}
diff --git a/pkg/deploy/generator/generators.go b/pkg/deploy/generator/generators.go
index 5cc44aeda..a2a63ac4e 100644
--- a/pkg/deploy/generator/generators.go
+++ b/pkg/deploy/generator/generators.go
@@ -27,11 +27,11 @@ func New(production bool) Generator {
 
 func (g *generator) Artifacts() error {
 	if g.production {
-		err := g.writeTemplate(g.managedIdentityTemplate(), FileRPProductionManagedIdentity)
+		err := g.writeTemplate(g.rpManagedIdentityTemplate(), FileRPProductionManagedIdentity)
 		if err != nil {
 			return err
 		}
-		err = g.writeTemplate(g.preDeployTemplate(), FileRPProductionPredeploy)
+		err = g.writeTemplate(g.rpPredeployTemplate(), FileRPProductionPredeploy)
 		if err != nil {
 			return err
 		}
@@ -55,7 +55,7 @@ func (g *generator) Artifacts() error {
 		if err != nil {
 			return err
 		}
-		err = g.writeParameters(g.rpPreDeployParameters(), FileRPProductionPredeployParameters)
+		err = g.writeParameters(g.rpPredeployParameters(), FileRPProductionPredeployParameters)
 		if err != nil {
 			return err
 		}
@@ -65,15 +65,15 @@ func (g *generator) Artifacts() error {
 		}
 
 	} else {
-		err := g.writeTemplate(g.sharedDevelopmentEnvTemplate(), fileEnvDevelopment)
+		err := g.writeTemplate(g.devSharedTemplate(), fileEnvDevelopment)
 		if err != nil {
 			return err
 		}
-		err = g.writeTemplate(g.databaseTemplate(), fileDatabaseDevelopment)
+		err = g.writeTemplate(g.devDatabaseTemplate(), fileDatabaseDevelopment)
 		if err != nil {
 			return err
 		}
-		err = g.writeTemplate(g.preDeployTemplate(), fileRPDevelopmentPredeploy)
+		err = g.writeTemplate(g.rpPredeployTemplate(), fileRPDevelopmentPredeploy)
 		if err != nil {
 			return err
 		}
diff --git a/pkg/deploy/generator/resources.go b/pkg/deploy/generator/resources.go
index 13d9d8d61..2ccb6a64e 100644
--- a/pkg/deploy/generator/resources.go
+++ b/pkg/deploy/generator/resources.go
@@ -4,408 +4,102 @@ package generator
 // Licensed under the Apache License 2.0.
 
 import (
-	"encoding/base64"
 	"fmt"
-	"strings"
 
-	mgmtcompute "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2020-06-01/compute"
-	mgmtdocumentdb "github.com/Azure/azure-sdk-for-go/services/cosmos-db/mgmt/2019-08-01/documentdb"
-	mgmtdns "github.com/Azure/azure-sdk-for-go/services/dns/mgmt/2018-05-01/dns"
-	mgmtkeyvault "github.com/Azure/azure-sdk-for-go/services/keyvault/mgmt/2016-10-01/keyvault"
-	mgmtmsi "github.com/Azure/azure-sdk-for-go/services/msi/mgmt/2018-11-30/msi"
 	mgmtnetwork "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2019-07-01/network"
-	mgmtauthorization "github.com/Azure/azure-sdk-for-go/services/preview/authorization/mgmt/2018-09-01-preview/authorization"
-	mgmtcontainerregistry "github.com/Azure/azure-sdk-for-go/services/preview/containerregistry/mgmt/2019-06-01-preview/containerregistry"
 	mgmtinsights "github.com/Azure/azure-sdk-for-go/services/preview/monitor/mgmt/2018-03-01/insights"
 	mgmtstorage "github.com/Azure/azure-sdk-for-go/services/storage/mgmt/2019-04-01/storage"
 	"github.com/Azure/go-autorest/autorest/to"
-	uuid "github.com/satori/go.uuid"
 
-	"github.com/Azure/ARO-RP/pkg/env"
 	"github.com/Azure/ARO-RP/pkg/util/arm"
 	"github.com/Azure/ARO-RP/pkg/util/azureclient"
-	"github.com/Azure/ARO-RP/pkg/util/rbac"
-	"github.com/Azure/ARO-RP/pkg/util/version"
 )
 
-const (
-	tenantIDHack = "13805ec3-a223-47ad-ad65-8b2baf92c0fb"
-)
-
-var (
-	tenantUUIDHack = uuid.Must(uuid.FromString(tenantIDHack))
-)
-
-func (g *generator) managedIdentity() *arm.Resource {
+func (g *generator) actionGroup(name string, shortName string) *arm.Resource {
 	return &arm.Resource{
-		Resource: &mgmtmsi.Identity{
-			Type:     to.StringPtr("Microsoft.ManagedIdentity/userAssignedIdentities"),
-			Name:     to.StringPtr("[concat('aro-rp-', resourceGroup().location)]"),
-			Location: to.StringPtr("[resourceGroup().location]"),
-		},
-		APIVersion: azureclient.APIVersion("Microsoft.ManagedIdentity"),
-	}
-}
-
-func (g *generator) securityGroupRP() *arm.Resource {
-	nsg := &mgmtnetwork.SecurityGroup{
-		SecurityGroupPropertiesFormat: &mgmtnetwork.SecurityGroupPropertiesFormat{
-			SecurityRules: &[]mgmtnetwork.SecurityRule{
-				{
-					SecurityRulePropertiesFormat: &mgmtnetwork.SecurityRulePropertiesFormat{
-						Protocol:                 mgmtnetwork.SecurityRuleProtocolTCP,
-						SourcePortRange:          to.StringPtr("*"),
-						DestinationPortRange:     to.StringPtr("443"),
-						SourceAddressPrefix:      to.StringPtr("AzureResourceManager"),
-						DestinationAddressPrefix: to.StringPtr("*"),
-						Access:                   mgmtnetwork.SecurityRuleAccessAllow,
-						Priority:                 to.Int32Ptr(120),
-						Direction:                mgmtnetwork.SecurityRuleDirectionInbound,
-					},
-					Name: to.StringPtr("rp_in_arm"),
-				},
+		Resource: mgmtinsights.ActionGroupResource{
+			ActionGroup: &mgmtinsights.ActionGroup{
+				Enabled:        to.BoolPtr(true),
+				GroupShortName: to.StringPtr(shortName),
 			},
+			Name:     to.StringPtr(name),
+			Type:     to.StringPtr("Microsoft.Insights/actionGroups"),
+			Location: to.StringPtr("Global"),
 		},
-		Name:     to.StringPtr("rp-nsg"),
-		Type:     to.StringPtr("Microsoft.Network/networkSecurityGroups"),
-		Location: to.StringPtr("[resourceGroup().location]"),
-	}
-
-	if !g.production {
-		// override production ARM flag for more open configuration in development
-		(*nsg.SecurityRules)[0].SecurityRulePropertiesFormat.SourceAddressPrefix = to.StringPtr("*")
-
-		*nsg.SecurityRules = append(*nsg.SecurityRules, mgmtnetwork.SecurityRule{
-			SecurityRulePropertiesFormat: &mgmtnetwork.SecurityRulePropertiesFormat{
-				Protocol:                 mgmtnetwork.SecurityRuleProtocolTCP,
-				SourcePortRange:          to.StringPtr("*"),
-				DestinationPortRange:     to.StringPtr("22"),
-				SourceAddressPrefix:      to.StringPtr("*"),
-				DestinationAddressPrefix: to.StringPtr("*"),
-				Access:                   mgmtnetwork.SecurityRuleAccessAllow,
-				Priority:                 to.Int32Ptr(125),
-				Direction:                mgmtnetwork.SecurityRuleDirectionInbound,
-			},
-			Name: to.StringPtr("ssh_in"),
-		})
-	} else {
-		*nsg.SecurityRules = append(*nsg.SecurityRules, mgmtnetwork.SecurityRule{
-			SecurityRulePropertiesFormat: &mgmtnetwork.SecurityRulePropertiesFormat{
-				Protocol:                 mgmtnetwork.SecurityRuleProtocolTCP,
-				SourcePortRange:          to.StringPtr("*"),
-				DestinationPortRange:     to.StringPtr("443"),
-				SourceAddressPrefixes:    to.StringSlicePtr([]string{}),
-				Access:                   mgmtnetwork.SecurityRuleAccessAllow,
-				DestinationAddressPrefix: to.StringPtr("*"),
-				Priority:                 to.Int32Ptr(130),
-				Direction:                mgmtnetwork.SecurityRuleDirectionInbound,
-			},
-			Name: to.StringPtr("rp_in_geneva"),
-		})
-	}
-
-	return &arm.Resource{
-		Resource:   nsg,
-		Condition:  g.conditionStanza("deployNSGs"),
-		APIVersion: azureclient.APIVersion("Microsoft.Network"),
+		APIVersion: azureclient.APIVersion("Microsoft.Insights"),
 	}
 }
 
-func (g *generator) securityGroupPE() *arm.Resource {
+func (g *generator) securityGroup(name string, securityRules *[]mgmtnetwork.SecurityRule, condition interface{}) *arm.Resource {
 	return &arm.Resource{
 		Resource: &mgmtnetwork.SecurityGroup{
-			SecurityGroupPropertiesFormat: &mgmtnetwork.SecurityGroupPropertiesFormat{},
-			Name:                          to.StringPtr("rp-pe-nsg"),
-			Type:                          to.StringPtr("Microsoft.Network/networkSecurityGroups"),
-			Location:                      to.StringPtr("[resourceGroup().location]"),
-		},
-		Condition:  g.conditionStanza("deployNSGs"),
-		APIVersion: azureclient.APIVersion("Microsoft.Network"),
-	}
-}
-
-func (g *generator) proxyVmss() *arm.Resource {
-	parts := []string{
-		fmt.Sprintf("base64ToString('%s')", base64.StdEncoding.EncodeToString([]byte("set -ex\n\n"))),
-	}
-
-	for _, variable := range []string{"proxyImage", "proxyImageAuth"} {
-		parts = append(parts,
-			fmt.Sprintf("'%s=$(base64 -d <<<'''", strings.ToUpper(variable)),
-			fmt.Sprintf("base64(parameters('%s'))", variable),
-			"''')\n'",
-		)
-	}
-
-	for _, variable := range []string{"proxyCert", "proxyClientCert", "proxyKey"} {
-		parts = append(parts,
-			fmt.Sprintf("'%s='''", strings.ToUpper(variable)),
-			fmt.Sprintf("parameters('%s')", variable),
-			"'''\n'",
-		)
-	}
-
-	trailer := base64.StdEncoding.EncodeToString([]byte(`yum -y update -x WALinuxAgent
-yum -y install docker
-
-firewall-cmd --add-port=443/tcp --permanent
-
-mkdir /root/.docker
-cat >/root/.docker/config.json <<EOF
-{
-	"auths": {
-		"${PROXYIMAGE%%/*}": {
-			"auth": "$PROXYIMAGEAUTH"
-		}
-	}
-}
-EOF
-systemctl start docker.service
-docker pull "$PROXYIMAGE"
-
-mkdir /etc/proxy
-base64 -d <<<"$PROXYCERT" >/etc/proxy/proxy.crt
-base64 -d <<<"$PROXYKEY" >/etc/proxy/proxy.key
-base64 -d <<<"$PROXYCLIENTCERT" >/etc/proxy/proxy-client.crt
-chown -R 1000:1000 /etc/proxy
-chmod 0600 /etc/proxy/proxy.key
-
-cat >/etc/sysconfig/proxy <<EOF
-PROXY_IMAGE='$PROXYIMAGE'
-EOF
-
-cat >/etc/systemd/system/proxy.service <<'EOF'
-[Unit]
-After=docker.service
-Requires=docker.service
-
-[Service]
-EnvironmentFile=/etc/sysconfig/proxy
-ExecStartPre=-/usr/bin/docker rm -f %n
-ExecStart=/usr/bin/docker run --rm --name %n -p 443:8443 -v /etc/proxy:/secrets $PROXY_IMAGE
-ExecStop=/usr/bin/docker stop %n
-Restart=always
-RestartSec=1
-StartLimitInterval=0
-
-[Install]
-WantedBy=multi-user.target
-EOF
-
-systemctl enable proxy.service
-
-(sleep 30; reboot) &
-`))
-
-	parts = append(parts, "'\n'", fmt.Sprintf("base64ToString('%s')", trailer))
-
-	script := fmt.Sprintf("[base64(concat(%s))]", strings.Join(parts, ","))
-
-	return &arm.Resource{
-		Resource: &mgmtcompute.VirtualMachineScaleSet{
-			Sku: &mgmtcompute.Sku{
-				Name:     to.StringPtr(string(mgmtcompute.VirtualMachineSizeTypesStandardD2sV3)),
-				Tier:     to.StringPtr("Standard"),
-				Capacity: to.Int64Ptr(1),
-			},
-			VirtualMachineScaleSetProperties: &mgmtcompute.VirtualMachineScaleSetProperties{
-				UpgradePolicy: &mgmtcompute.UpgradePolicy{
-					Mode: mgmtcompute.UpgradeModeManual,
-				},
-				VirtualMachineProfile: &mgmtcompute.VirtualMachineScaleSetVMProfile{
-					OsProfile: &mgmtcompute.VirtualMachineScaleSetOSProfile{
-						ComputerNamePrefix: to.StringPtr("dev-proxy-"),
-						AdminUsername:      to.StringPtr("cloud-user"),
-						LinuxConfiguration: &mgmtcompute.LinuxConfiguration{
-							DisablePasswordAuthentication: to.BoolPtr(true),
-							SSH: &mgmtcompute.SSHConfiguration{
-								PublicKeys: &[]mgmtcompute.SSHPublicKey{
-									{
-										Path:    to.StringPtr("/home/cloud-user/.ssh/authorized_keys"),
-										KeyData: to.StringPtr("[parameters('sshPublicKey')]"),
-									},
-								},
-							},
-						},
-					},
-					StorageProfile: &mgmtcompute.VirtualMachineScaleSetStorageProfile{
-						ImageReference: &mgmtcompute.ImageReference{
-							Publisher: to.StringPtr("RedHat"),
-							Offer:     to.StringPtr("RHEL"),
-							Sku:       to.StringPtr("7-LVM"),
-							Version:   to.StringPtr("latest"),
-						},
-						OsDisk: &mgmtcompute.VirtualMachineScaleSetOSDisk{
-							CreateOption: mgmtcompute.DiskCreateOptionTypesFromImage,
-							ManagedDisk: &mgmtcompute.VirtualMachineScaleSetManagedDiskParameters{
-								StorageAccountType: mgmtcompute.StorageAccountTypesPremiumLRS,
-							},
-						},
-					},
-					NetworkProfile: &mgmtcompute.VirtualMachineScaleSetNetworkProfile{
-						NetworkInterfaceConfigurations: &[]mgmtcompute.VirtualMachineScaleSetNetworkConfiguration{
-							{
-								Name: to.StringPtr("dev-proxy-vmss-nic"),
-								VirtualMachineScaleSetNetworkConfigurationProperties: &mgmtcompute.VirtualMachineScaleSetNetworkConfigurationProperties{
-									Primary: to.BoolPtr(true),
-									IPConfigurations: &[]mgmtcompute.VirtualMachineScaleSetIPConfiguration{
-										{
-											Name: to.StringPtr("dev-proxy-vmss-ipconfig"),
-											VirtualMachineScaleSetIPConfigurationProperties: &mgmtcompute.VirtualMachineScaleSetIPConfigurationProperties{
-												Subnet: &mgmtcompute.APIEntityReference{
-													ID: to.StringPtr("[resourceId('Microsoft.Network/virtualNetworks/subnets', 'rp-vnet', 'rp-subnet')]"),
-												},
-												Primary: to.BoolPtr(true),
-												PublicIPAddressConfiguration: &mgmtcompute.VirtualMachineScaleSetPublicIPAddressConfiguration{
-													Name: to.StringPtr("dev-proxy-vmss-pip"),
-													VirtualMachineScaleSetPublicIPAddressConfigurationProperties: &mgmtcompute.VirtualMachineScaleSetPublicIPAddressConfigurationProperties{
-														DNSSettings: &mgmtcompute.VirtualMachineScaleSetPublicIPAddressConfigurationDNSSettings{
-															DomainNameLabel: to.StringPtr("[parameters('proxyDomainNameLabel')]"),
-														},
-													},
-												},
-											},
-										},
-									},
-								},
-							},
-						},
-					},
-					ExtensionProfile: &mgmtcompute.VirtualMachineScaleSetExtensionProfile{
-						Extensions: &[]mgmtcompute.VirtualMachineScaleSetExtension{
-							{
-								Name: to.StringPtr("dev-proxy-vmss-cse"),
-								VirtualMachineScaleSetExtensionProperties: &mgmtcompute.VirtualMachineScaleSetExtensionProperties{
-									Publisher:               to.StringPtr("Microsoft.Azure.Extensions"),
-									Type:                    to.StringPtr("CustomScript"),
-									TypeHandlerVersion:      to.StringPtr("2.0"),
-									AutoUpgradeMinorVersion: to.BoolPtr(true),
-									Settings:                map[string]interface{}{},
-									ProtectedSettings: map[string]interface{}{
-										"script": script,
-									},
-								},
-							},
-						},
-					},
-				},
-				Overprovision: to.BoolPtr(false),
+			SecurityGroupPropertiesFormat: &mgmtnetwork.SecurityGroupPropertiesFormat{
+				SecurityRules: securityRules,
 			},
-			Name:     to.StringPtr("dev-proxy-vmss"),
-			Type:     to.StringPtr("Microsoft.Compute/virtualMachineScaleSets"),
+			Name:     &name,
+			Type:     to.StringPtr("Microsoft.Network/networkSecurityGroups"),
 			Location: to.StringPtr("[resourceGroup().location]"),
 		},
-		APIVersion: azureclient.APIVersion("Microsoft.Compute"),
+		Condition:  condition,
+		APIVersion: azureclient.APIVersion("Microsoft.Network"),
 	}
 }
 
-func (g *generator) devVpnPip() *arm.Resource {
+func (g *generator) publicIPAddress(name string) *arm.Resource {
 	return &arm.Resource{
 		Resource: &mgmtnetwork.PublicIPAddress{
 			Sku: &mgmtnetwork.PublicIPAddressSku{
-				Name: "[parameters('publicIPAddressSkuName')]",
+				Name: mgmtnetwork.PublicIPAddressSkuNameStandard,
 			},
 			PublicIPAddressPropertiesFormat: &mgmtnetwork.PublicIPAddressPropertiesFormat{
-				PublicIPAllocationMethod: "[parameters('publicIPAddressAllocationMethod')]",
+				PublicIPAllocationMethod: mgmtnetwork.Static,
 			},
-			Name:     to.StringPtr("dev-vpn-pip"),
+			Name:     &name,
 			Type:     to.StringPtr("Microsoft.Network/publicIPAddresses"),
 			Location: to.StringPtr("[resourceGroup().location]"),
 		},
-		Condition:  "[equals(parameters('ciCapacity'), 0)]", // TODO(mj): Refactor g.conditionStanza for better usage
 		APIVersion: azureclient.APIVersion("Microsoft.Network"),
 	}
 }
 
-func (g *generator) devVnet() *arm.Resource {
+func (g *generator) storageAccount(name string, accountProperties *mgmtstorage.AccountProperties) *arm.Resource {
 	return &arm.Resource{
-		Resource: &mgmtnetwork.VirtualNetwork{
-			VirtualNetworkPropertiesFormat: &mgmtnetwork.VirtualNetworkPropertiesFormat{
-				AddressSpace: &mgmtnetwork.AddressSpace{
-					AddressPrefixes: &[]string{
-						"10.0.0.0/23",
-					},
-				},
-				Subnets: &[]mgmtnetwork.Subnet{
-					{
-						SubnetPropertiesFormat: &mgmtnetwork.SubnetPropertiesFormat{
-							AddressPrefix: to.StringPtr("10.0.0.0/24"),
-						},
-						Name: to.StringPtr("GatewaySubnet"),
-					},
-					{
-						SubnetPropertiesFormat: &mgmtnetwork.SubnetPropertiesFormat{
-							AddressPrefix: to.StringPtr("10.0.1.0/24"),
-							NetworkSecurityGroup: &mgmtnetwork.SecurityGroup{
-								ID: to.StringPtr("[resourceId('Microsoft.Network/networkSecurityGroups', 'rp-nsg')]"),
-							},
-						},
-						Name: to.StringPtr("ToolingSubnet"),
-					},
-				},
-			},
-			Name:     to.StringPtr("dev-vnet"),
-			Type:     to.StringPtr("Microsoft.Network/virtualNetworks"),
+		Resource: &mgmtstorage.Account{
+			Name:     &name,
+			Type:     to.StringPtr("Microsoft.Storage/storageAccounts"),
 			Location: to.StringPtr("[resourceGroup().location]"),
+			Sku: &mgmtstorage.Sku{
+				Name: "Standard_LRS",
+			},
+			AccountProperties: accountProperties,
 		},
-		APIVersion: azureclient.APIVersion("Microsoft.Network"),
+		APIVersion: azureclient.APIVersion("Microsoft.Storage"),
 	}
 }
 
-func (g *generator) devVPN() *arm.Resource {
+func (g *generator) virtualNetwork(name, addressPrefix string, subnets *[]mgmtnetwork.Subnet, condition interface{}) *arm.Resource {
 	return &arm.Resource{
-		Resource: &mgmtnetwork.VirtualNetworkGateway{
-			VirtualNetworkGatewayPropertiesFormat: &mgmtnetwork.VirtualNetworkGatewayPropertiesFormat{
-				IPConfigurations: &[]mgmtnetwork.VirtualNetworkGatewayIPConfiguration{
-					{
-						VirtualNetworkGatewayIPConfigurationPropertiesFormat: &mgmtnetwork.VirtualNetworkGatewayIPConfigurationPropertiesFormat{
-							Subnet: &mgmtnetwork.SubResource{
-								ID: to.StringPtr("[resourceId('Microsoft.Network/virtualNetworks/subnets', 'dev-vnet', 'GatewaySubnet')]"),
-							},
-							PublicIPAddress: &mgmtnetwork.SubResource{
-								ID: to.StringPtr("[resourceId('Microsoft.Network/publicIPAddresses', 'dev-vpn-pip')]"),
-							},
-						},
-						Name: to.StringPtr("default"),
-					},
-				},
-				VpnType: mgmtnetwork.RouteBased,
-				Sku: &mgmtnetwork.VirtualNetworkGatewaySku{
-					Name: mgmtnetwork.VirtualNetworkGatewaySkuNameVpnGw1,
-					Tier: mgmtnetwork.VirtualNetworkGatewaySkuTierVpnGw1,
-				},
-				VpnClientConfiguration: &mgmtnetwork.VpnClientConfiguration{
-					VpnClientAddressPool: &mgmtnetwork.AddressSpace{
-						AddressPrefixes: &[]string{"192.168.255.0/24"},
-					},
-					VpnClientRootCertificates: &[]mgmtnetwork.VpnClientRootCertificate{
-						{
-							VpnClientRootCertificatePropertiesFormat: &mgmtnetwork.VpnClientRootCertificatePropertiesFormat{
-								PublicCertData: to.StringPtr("[parameters('vpnCACertificate')]"),
-							},
-							Name: to.StringPtr("dev-vpn-ca"),
-						},
-					},
-					VpnClientProtocols: &[]mgmtnetwork.VpnClientProtocol{
-						mgmtnetwork.OpenVPN,
+		Resource: &mgmtnetwork.VirtualNetwork{
+			VirtualNetworkPropertiesFormat: &mgmtnetwork.VirtualNetworkPropertiesFormat{
+				AddressSpace: &mgmtnetwork.AddressSpace{
+					AddressPrefixes: &[]string{
+						addressPrefix,
 					},
 				},
+				Subnets: subnets,
 			},
-			Name:     to.StringPtr("dev-vpn"),
-			Type:     to.StringPtr("Microsoft.Network/virtualNetworkGateways"),
+			Name:     &name,
+			Type:     to.StringPtr("Microsoft.Network/virtualNetworks"),
 			Location: to.StringPtr("[resourceGroup().location]"),
 		},
-		Condition:  "[equals(parameters('ciCapacity'), 0)]", // TODO(mj): Refactor g.conditionStanza for better usage
+		Condition:  condition,
 		APIVersion: azureclient.APIVersion("Microsoft.Network"),
-		DependsOn: []string{
-			"[resourceId('Microsoft.Network/publicIPAddresses', 'dev-vpn-pip')]",
-			"[resourceId('Microsoft.Network/virtualNetworks', 'dev-vnet')]",
-		},
 	}
 }
 
-// halfPeering configures vnetA to peer with vnetB, two symmetrical configurations have to be applied for a peering to work
-func (g *generator) halfPeering(vnetA string, vnetB string) *arm.Resource {
+// virtualNetworkPeering configures vnetA to peer with vnetB, two symmetrical
+// configurations have to be applied for a peering to work
+func (g *generator) virtualNetworkPeering(vnetA, vnetB string) *arm.Resource {
 	return &arm.Resource{
 		Resource: &mgmtnetwork.VirtualNetworkPeering{
 			VirtualNetworkPeeringPropertiesFormat: &mgmtnetwork.VirtualNetworkPeeringPropertiesFormat{
@@ -428,1658 +122,3 @@ func (g *generator) halfPeering(vnetA string, vnetB string) *arm.Resource {
 		Location: "[resourceGroup().location]",
 	}
 }
-
-func (g *generator) rpvnet() *arm.Resource {
-	subnet := mgmtnetwork.Subnet{
-		SubnetPropertiesFormat: &mgmtnetwork.SubnetPropertiesFormat{
-			AddressPrefix: to.StringPtr("10.0.0.0/24"),
-			NetworkSecurityGroup: &mgmtnetwork.SecurityGroup{
-				ID: to.StringPtr("[resourceId('Microsoft.Network/networkSecurityGroups', 'rp-nsg')]"),
-			},
-		},
-		Name: to.StringPtr("rp-subnet"),
-	}
-
-	if g.production {
-		subnet.ServiceEndpoints = &[]mgmtnetwork.ServiceEndpointPropertiesFormat{
-			{
-				Service:   to.StringPtr("Microsoft.KeyVault"),
-				Locations: &[]string{"*"},
-			},
-			{
-				Service:   to.StringPtr("Microsoft.AzureCosmosDB"),
-				Locations: &[]string{"*"},
-			},
-		}
-	}
-
-	return &arm.Resource{
-		Resource: &mgmtnetwork.VirtualNetwork{
-			VirtualNetworkPropertiesFormat: &mgmtnetwork.VirtualNetworkPropertiesFormat{
-				AddressSpace: &mgmtnetwork.AddressSpace{
-					AddressPrefixes: &[]string{
-						"10.0.0.0/24",
-					},
-				},
-				Subnets: &[]mgmtnetwork.Subnet{
-					subnet,
-				},
-			},
-			Name:     to.StringPtr("rp-vnet"),
-			Type:     to.StringPtr("Microsoft.Network/virtualNetworks"),
-			Location: to.StringPtr("[resourceGroup().location]"),
-		},
-		APIVersion: azureclient.APIVersion("Microsoft.Network"),
-	}
-}
-
-func (g *generator) pevnet() *arm.Resource {
-	return &arm.Resource{
-		Resource: &mgmtnetwork.VirtualNetwork{
-			VirtualNetworkPropertiesFormat: &mgmtnetwork.VirtualNetworkPropertiesFormat{
-				AddressSpace: &mgmtnetwork.AddressSpace{
-					AddressPrefixes: &[]string{
-						"10.0.4.0/22",
-					},
-				},
-				Subnets: &[]mgmtnetwork.Subnet{
-					{
-						SubnetPropertiesFormat: &mgmtnetwork.SubnetPropertiesFormat{
-							AddressPrefix: to.StringPtr("10.0.4.0/22"),
-							NetworkSecurityGroup: &mgmtnetwork.SecurityGroup{
-								ID: to.StringPtr("[resourceId('Microsoft.Network/networkSecurityGroups', 'rp-pe-nsg')]"),
-							},
-							PrivateEndpointNetworkPolicies: to.StringPtr("Disabled"),
-						},
-						Name: to.StringPtr("rp-pe-subnet"),
-					},
-				},
-			},
-			Name:     to.StringPtr("rp-pe-vnet-001"),
-			Type:     to.StringPtr("Microsoft.Network/virtualNetworks"),
-			Location: to.StringPtr("[resourceGroup().location]"),
-		},
-		APIVersion: azureclient.APIVersion("Microsoft.Network"),
-	}
-}
-
-func (g *generator) pip(name string) *arm.Resource {
-	return &arm.Resource{
-		Resource: &mgmtnetwork.PublicIPAddress{
-			Sku: &mgmtnetwork.PublicIPAddressSku{
-				Name: mgmtnetwork.PublicIPAddressSkuNameStandard,
-			},
-			PublicIPAddressPropertiesFormat: &mgmtnetwork.PublicIPAddressPropertiesFormat{
-				PublicIPAllocationMethod: mgmtnetwork.Static,
-			},
-			Name:     &name,
-			Type:     to.StringPtr("Microsoft.Network/publicIPAddresses"),
-			Location: to.StringPtr("[resourceGroup().location]"),
-		},
-		APIVersion: azureclient.APIVersion("Microsoft.Network"),
-	}
-}
-
-func (g *generator) lb() *arm.Resource {
-	return &arm.Resource{
-		Resource: &mgmtnetwork.LoadBalancer{
-			Sku: &mgmtnetwork.LoadBalancerSku{
-				Name: mgmtnetwork.LoadBalancerSkuNameStandard,
-			},
-			LoadBalancerPropertiesFormat: &mgmtnetwork.LoadBalancerPropertiesFormat{
-				FrontendIPConfigurations: &[]mgmtnetwork.FrontendIPConfiguration{
-					{
-						FrontendIPConfigurationPropertiesFormat: &mgmtnetwork.FrontendIPConfigurationPropertiesFormat{
-							PublicIPAddress: &mgmtnetwork.PublicIPAddress{
-								ID: to.StringPtr("[resourceId('Microsoft.Network/publicIPAddresses', 'rp-pip')]"),
-							},
-						},
-						Name: to.StringPtr("rp-frontend"),
-					},
-					{
-						FrontendIPConfigurationPropertiesFormat: &mgmtnetwork.FrontendIPConfigurationPropertiesFormat{
-							PublicIPAddress: &mgmtnetwork.PublicIPAddress{
-								ID: to.StringPtr("[resourceId('Microsoft.Network/publicIPAddresses', 'portal-pip')]"),
-							},
-						},
-						Name: to.StringPtr("portal-frontend"),
-					},
-				},
-				BackendAddressPools: &[]mgmtnetwork.BackendAddressPool{
-					{
-						Name: to.StringPtr("rp-backend"),
-					},
-				},
-				LoadBalancingRules: &[]mgmtnetwork.LoadBalancingRule{
-					{
-						LoadBalancingRulePropertiesFormat: &mgmtnetwork.LoadBalancingRulePropertiesFormat{
-							FrontendIPConfiguration: &mgmtnetwork.SubResource{
-								ID: to.StringPtr("[resourceId('Microsoft.Network/loadBalancers/frontendIPConfigurations', 'rp-lb', 'rp-frontend')]"),
-							},
-							BackendAddressPool: &mgmtnetwork.SubResource{
-								ID: to.StringPtr("[resourceId('Microsoft.Network/loadBalancers/backendAddressPools', 'rp-lb', 'rp-backend')]"),
-							},
-							Probe: &mgmtnetwork.SubResource{
-								ID: to.StringPtr("[resourceId('Microsoft.Network/loadBalancers/probes', 'rp-lb', 'rp-probe')]"),
-							},
-							Protocol:         mgmtnetwork.TransportProtocolTCP,
-							LoadDistribution: mgmtnetwork.LoadDistributionDefault,
-							FrontendPort:     to.Int32Ptr(443),
-							BackendPort:      to.Int32Ptr(443),
-						},
-						Name: to.StringPtr("rp-lbrule"),
-					},
-					{
-						LoadBalancingRulePropertiesFormat: &mgmtnetwork.LoadBalancingRulePropertiesFormat{
-							FrontendIPConfiguration: &mgmtnetwork.SubResource{
-								ID: to.StringPtr("[resourceId('Microsoft.Network/loadBalancers/frontendIPConfigurations', 'rp-lb', 'portal-frontend')]"),
-							},
-							BackendAddressPool: &mgmtnetwork.SubResource{
-								ID: to.StringPtr("[resourceId('Microsoft.Network/loadBalancers/backendAddressPools', 'rp-lb', 'rp-backend')]"),
-							},
-							Probe: &mgmtnetwork.SubResource{
-								ID: to.StringPtr("[resourceId('Microsoft.Network/loadBalancers/probes', 'rp-lb', 'portal-probe-https')]"),
-							},
-							Protocol:         mgmtnetwork.TransportProtocolTCP,
-							LoadDistribution: mgmtnetwork.LoadDistributionDefault,
-							FrontendPort:     to.Int32Ptr(443),
-							BackendPort:      to.Int32Ptr(444),
-						},
-						Name: to.StringPtr("portal-lbrule"),
-					},
-					{
-						LoadBalancingRulePropertiesFormat: &mgmtnetwork.LoadBalancingRulePropertiesFormat{
-							FrontendIPConfiguration: &mgmtnetwork.SubResource{
-								ID: to.StringPtr("[resourceId('Microsoft.Network/loadBalancers/frontendIPConfigurations', 'rp-lb', 'portal-frontend')]"),
-							},
-							BackendAddressPool: &mgmtnetwork.SubResource{
-								ID: to.StringPtr("[resourceId('Microsoft.Network/loadBalancers/backendAddressPools', 'rp-lb', 'rp-backend')]"),
-							},
-							Probe: &mgmtnetwork.SubResource{
-								ID: to.StringPtr("[resourceId('Microsoft.Network/loadBalancers/probes', 'rp-lb', 'portal-probe-ssh')]"),
-							},
-							Protocol:         mgmtnetwork.TransportProtocolTCP,
-							LoadDistribution: mgmtnetwork.LoadDistributionDefault,
-							FrontendPort:     to.Int32Ptr(22),
-							BackendPort:      to.Int32Ptr(2222),
-						},
-						Name: to.StringPtr("portal-lbrule-ssh"),
-					},
-				},
-				Probes: &[]mgmtnetwork.Probe{
-					{
-						ProbePropertiesFormat: &mgmtnetwork.ProbePropertiesFormat{
-							Protocol:       mgmtnetwork.ProbeProtocolHTTPS,
-							Port:           to.Int32Ptr(443),
-							NumberOfProbes: to.Int32Ptr(2),
-							RequestPath:    to.StringPtr("/healthz/ready"),
-						},
-						Name: to.StringPtr("rp-probe"),
-					},
-					{
-						ProbePropertiesFormat: &mgmtnetwork.ProbePropertiesFormat{
-							Protocol:       mgmtnetwork.ProbeProtocolHTTPS,
-							Port:           to.Int32Ptr(444),
-							NumberOfProbes: to.Int32Ptr(2),
-							RequestPath:    to.StringPtr("/healthz/ready"),
-						},
-						Name: to.StringPtr("portal-probe-https"),
-					},
-					{
-						ProbePropertiesFormat: &mgmtnetwork.ProbePropertiesFormat{
-							Protocol:       mgmtnetwork.ProbeProtocolTCP,
-							Port:           to.Int32Ptr(2222),
-							NumberOfProbes: to.Int32Ptr(2),
-						},
-						Name: to.StringPtr("portal-probe-ssh"),
-					},
-				},
-			},
-			Name:     to.StringPtr("rp-lb"),
-			Type:     to.StringPtr("Microsoft.Network/loadBalancers"),
-			Location: to.StringPtr("[resourceGroup().location]"),
-		},
-		APIVersion: azureclient.APIVersion("Microsoft.Network"),
-		DependsOn: []string{
-			"[resourceId('Microsoft.Network/publicIPAddresses', 'portal-pip')]",
-			"[resourceId('Microsoft.Network/publicIPAddresses', 'rp-pip')]",
-		},
-	}
-}
-
-func (g *generator) actionGroup(name string, shortName string) *arm.Resource {
-	return &arm.Resource{
-		Resource: mgmtinsights.ActionGroupResource{
-			ActionGroup: &mgmtinsights.ActionGroup{
-				Enabled:        to.BoolPtr(true),
-				GroupShortName: to.StringPtr(shortName),
-			},
-			Name:     to.StringPtr(name),
-			Type:     to.StringPtr("Microsoft.Insights/actionGroups"),
-			Location: to.StringPtr("Global"),
-		},
-		APIVersion: azureclient.APIVersion("Microsoft.Insights"),
-	}
-}
-
-// lbAlert generates an alert resource for the rp-lb healthprobe metric
-func (g *generator) lbAlert(threshold float64, severity int32, name string, evalFreq string, windowSize string, metric string) *arm.Resource {
-	return &arm.Resource{
-		Resource: mgmtinsights.MetricAlertResource{
-			MetricAlertProperties: &mgmtinsights.MetricAlertProperties{
-				Actions: &[]mgmtinsights.MetricAlertAction{
-					{
-						ActionGroupID: to.StringPtr("[resourceId(parameters('subscriptionResourceGroupName'), 'Microsoft.Insights/actionGroups', 'rp-health-ag')]"),
-					},
-				},
-				Enabled:             to.BoolPtr(true),
-				EvaluationFrequency: to.StringPtr(evalFreq),
-				Severity:            to.Int32Ptr(severity),
-				Scopes: &[]string{
-					"[resourceId('Microsoft.Network/loadBalancers', 'rp-lb')]",
-				},
-				WindowSize:         to.StringPtr(windowSize),
-				TargetResourceType: to.StringPtr("Microsoft.Network/loadBalancers"),
-				AutoMitigate:       to.BoolPtr(true),
-				Criteria: mgmtinsights.MetricAlertSingleResourceMultipleMetricCriteria{
-					AllOf: &[]mgmtinsights.MetricCriteria{
-						{
-							CriterionType:   mgmtinsights.CriterionTypeStaticThresholdCriterion,
-							MetricName:      to.StringPtr(metric),
-							MetricNamespace: to.StringPtr("microsoft.network/loadBalancers"),
-							Name:            to.StringPtr("HealthProbeCheck"),
-							Operator:        mgmtinsights.OperatorLessThan,
-							Threshold:       to.Float64Ptr(threshold),
-							TimeAggregation: mgmtinsights.Average,
-						},
-					},
-					OdataType: mgmtinsights.OdataTypeMicrosoftAzureMonitorSingleResourceMultipleMetricCriteria,
-				},
-			},
-			Name:     to.StringPtr("[concat('" + name + "-', resourceGroup().location)]"),
-			Type:     to.StringPtr("Microsoft.Insights/metricAlerts"),
-			Location: to.StringPtr("global"),
-		},
-		APIVersion: azureclient.APIVersion("Microsoft.Insights"),
-		DependsOn: []string{
-			"[resourceId('Microsoft.Network/loadBalancers', 'rp-lb')]",
-		},
-	}
-}
-
-func (g *generator) vmss() *arm.Resource {
-	parts := []string{
-		fmt.Sprintf("base64ToString('%s')", base64.StdEncoding.EncodeToString([]byte("set -ex\n\n"))),
-	}
-
-	for _, variable := range []string{
-		"mdmFrontendUrl",
-		"mdsdConfigVersion",
-		"mdsdEnvironment",
-		"acrResourceId",
-		"clusterParentDomainName",
-		"portalAccessGroupIds",
-		"portalClientId",
-		"portalElevatedGroupIds",
-		"rpImage",
-		"rpMode",
-		"rpParentDomainName",
-		"adminApiClientCertCommonName",
-		"databaseAccountName",
-		"keyvaultPrefix",
-	} {
-		parts = append(parts,
-			fmt.Sprintf("'%s=$(base64 -d <<<'''", strings.ToUpper(variable)),
-			fmt.Sprintf("base64(parameters('%s'))", variable),
-			"''')\n'",
-		)
-	}
-
-	for _, variable := range []string{
-		"adminApiCaBundle",
-	} {
-		parts = append(parts,
-			fmt.Sprintf("'%s='''", strings.ToUpper(variable)),
-			fmt.Sprintf("parameters('%s')", variable),
-			"'''\n'",
-		)
-	}
-
-	parts = append(parts,
-		"'MDMIMAGE=''"+version.MdmImage("")+"''\n'",
-	)
-
-	parts = append(parts,
-		"'LOCATION=$(base64 -d <<<'''",
-		"base64(resourceGroup().location)",
-		"''')\n'",
-	)
-
-	parts = append(parts,
-		"'SUBSCRIPTIONID=$(base64 -d <<<'''",
-		"base64(subscription().subscriptionId)",
-		"''')\n'",
-	)
-
-	parts = append(parts,
-		"'RESOURCEGROUPNAME=$(base64 -d <<<'''",
-		"base64(resourceGroup().name)",
-		"''')\n'",
-	)
-
-	trailer := base64.StdEncoding.EncodeToString([]byte(`
-yum -y update -x WALinuxAgent
-
-lvextend -l +50%FREE /dev/rootvg/rootlv
-xfs_growfs /
-
-lvextend -l +100%FREE /dev/rootvg/varlv
-xfs_growfs /var
-
-# avoid "error: db5 error(-30969) from dbenv->open: BDB0091 DB_VERSION_MISMATCH: Database environment version mismatch"
-rm -f /var/lib/rpm/__db*
-
-rpm --import https://dl.fedoraproject.org/pub/epel/RPM-GPG-KEY-EPEL-7
-rpm --import https://packages.microsoft.com/keys/microsoft.asc
-rpm --import https://packages.fluentbit.io/fluentbit.key
-
-for attempt in {1..5}; do
-  yum -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm && break
-  if [[ ${attempt} -lt 5 ]]; then sleep 10; else exit 1; fi
-done
-
-cat >/etc/yum.repos.d/azure.repo <<'EOF'
-[azure-cli]
-name=azure-cli
-baseurl=https://packages.microsoft.com/yumrepos/azure-cli
-enabled=yes
-gpgcheck=yes
-
-[azurecore]
-name=azurecore
-baseurl=https://packages.microsoft.com/yumrepos/azurecore
-enabled=yes
-gpgcheck=no
-EOF
-
-cat >/etc/yum.repos.d/td-agent-bit.repo <<'EOF'
-[td-agent-bit]
-name=td-agent-bit
-baseurl=https://packages.fluentbit.io/centos/7/$basearch
-enabled=yes
-gpgcheck=yes
-EOF
-
-for attempt in {1..5}; do
-yum --enablerepo=rhui-rhel-7-server-rhui-optional-rpms -y install azsec-clamav azsec-monitor azure-cli azure-mdsd azure-security docker openssl-perl td-agent-bit && break
-  if [[ ${attempt} -lt 5 ]]; then sleep 10; else exit 1; fi
-done
-
-rpm -e $(rpm -qa | grep ^abrt-)
-cat >/etc/sysctl.d/01-disable-core.conf <<'EOF'
-kernel.core_pattern = |/bin/true
-EOF
-sysctl --system
-
-firewall-cmd --add-port=443/tcp --permanent
-firewall-cmd --add-port=444/tcp --permanent
-firewall-cmd --add-port=2222/tcp --permanent
-
-cat >/etc/td-agent-bit/td-agent-bit.conf <<'EOF'
-[INPUT]
-	Name systemd
-	Tag journald
-	Systemd_Filter _COMM=aro
-
-[FILTER]
-	Name modify
-	Match journald
-	Remove_wildcard _
-	Remove TIMESTAMP
-
-[FILTER]
-	Name rewrite_tag
-	Match journald
-	Rule $LOGKIND ifxaudit ifxaudit false
-
-[OUTPUT]
-	Name forward
-	Match *
-	Port 29230
-EOF
-
-az login -i
-az account set -s "$SUBSCRIPTIONID"
-
-systemctl start docker.service
-az acr login --name "$(sed -e 's|.*/||' <<<"$ACRRESOURCEID")"
-
-MDMIMAGE="${RPIMAGE%%/*}/${MDMIMAGE##*/}"
-docker pull "$MDMIMAGE"
-docker pull "$RPIMAGE"
-
-az logout
-
-mkdir /etc/aro-rp
-base64 -d <<<"$ADMINAPICABUNDLE" >/etc/aro-rp/admin-ca-bundle.pem
-chown -R 1000:1000 /etc/aro-rp
-
-cat >/etc/sysconfig/mdm <<EOF
-MDMFRONTENDURL='$MDMFRONTENDURL'
-MDMIMAGE='$MDMIMAGE'
-MDMSOURCEENVIRONMENT='$LOCATION'
-MDMSOURCEROLE=rp
-MDMSOURCEROLEINSTANCE='$(hostname)'
-EOF
-
-mkdir /var/etw
-cat >/etc/systemd/system/mdm.service <<'EOF'
-[Unit]
-After=docker.service
-Requires=docker.service
-
-[Service]
-EnvironmentFile=/etc/sysconfig/mdm
-ExecStartPre=-/usr/bin/docker rm -f %N
-ExecStart=/usr/bin/docker run \
-  --entrypoint /usr/sbin/MetricsExtension \
-  --hostname %H \
-  --name %N \
-  --rm \
-  -m 2g \
-  -v /etc/mdm.pem:/etc/mdm.pem \
-  -v /var/etw:/var/etw:z \
-  $MDMIMAGE \
-  -CertFile /etc/mdm.pem \
-  -FrontEndUrl $MDMFRONTENDURL \
-  -Logger Console \
-  -LogLevel Warning \
-  -PrivateKeyFile /etc/mdm.pem \
-  -SourceEnvironment $MDMSOURCEENVIRONMENT \
-  -SourceRole $MDMSOURCEROLE \
-  -SourceRoleInstance $MDMSOURCEROLEINSTANCE
-ExecStop=/usr/bin/docker stop %N
-Restart=always
-RestartSec=1
-StartLimitInterval=0
-
-[Install]
-WantedBy=multi-user.target
-EOF
-
-cat >/etc/sysconfig/aro-rp <<EOF
-ACR_RESOURCE_ID='$ACRRESOURCEID'
-ADMIN_API_CLIENT_CERT_COMMON_NAME='$ADMINAPICLIENTCERTCOMMONNAME'
-DATABASE_ACCOUNT_NAME='$DATABASEACCOUNTNAME'
-DOMAIN_NAME='$LOCATION.$CLUSTERPARENTDOMAINNAME'
-KEYVAULT_PREFIX='$KEYVAULTPREFIX'
-MDM_ACCOUNT=AzureRedHatOpenShiftRP
-MDM_NAMESPACE=RP
-RP_MODE='$RPMODE'
-RPIMAGE='$RPIMAGE'
-EOF
-
-cat >/etc/systemd/system/aro-rp.service <<'EOF'
-[Unit]
-After=docker.service
-Requires=docker.service
-
-[Service]
-EnvironmentFile=/etc/sysconfig/aro-rp
-ExecStartPre=-/usr/bin/docker rm -f %N
-ExecStart=/usr/bin/docker run \
-  --hostname %H \
-  --name %N \
-  --rm \
-  -e ACR_RESOURCE_ID \
-  -e ADMIN_API_CLIENT_CERT_COMMON_NAME \
-  -e DATABASE_ACCOUNT_NAME \
-  -e DOMAIN_NAME \
-  -e KEYVAULT_PREFIX \
-  -e MDM_ACCOUNT \
-  -e MDM_NAMESPACE \
-  -e RP_MODE \
-  -m 2g \
-  -p 443:8443 \
-  -v /etc/aro-rp:/etc/aro-rp \
-  -v /run/systemd/journal:/run/systemd/journal \
-  -v /var/etw:/var/etw:z \
-  $RPIMAGE \
-  rp
-ExecStop=/usr/bin/docker stop -t 3600 %N
-TimeoutStopSec=3600
-Restart=always
-RestartSec=1
-StartLimitInterval=0
-
-[Install]
-WantedBy=multi-user.target
-EOF
-
-cat >/etc/sysconfig/aro-monitor <<EOF
-CLUSTER_MDM_ACCOUNT=AzureRedHatOpenShiftCluster
-CLUSTER_MDM_NAMESPACE=BBM
-DATABASE_ACCOUNT_NAME='$DATABASEACCOUNTNAME'
-KEYVAULT_PREFIX='$KEYVAULTPREFIX'
-MDM_ACCOUNT=AzureRedHatOpenShiftRP
-MDM_NAMESPACE=BBM
-RP_MODE='$RPMODE'
-RPIMAGE='$RPIMAGE'
-EOF
-
-cat >/etc/systemd/system/aro-monitor.service <<'EOF'
-[Unit]
-After=docker.service
-Requires=docker.service
-
-[Service]
-EnvironmentFile=/etc/sysconfig/aro-monitor
-ExecStartPre=-/usr/bin/docker rm -f %N
-ExecStart=/usr/bin/docker run \
-  --hostname %H \
-  --name %N \
-  --rm \
-  -e CLUSTER_MDM_ACCOUNT \
-  -e CLUSTER_MDM_NAMESPACE \
-  -e DATABASE_ACCOUNT_NAME \
-  -e KEYVAULT_PREFIX \
-  -e MDM_ACCOUNT \
-  -e MDM_NAMESPACE \
-  -e RP_MODE \
-  -m 2g \
-  -v /run/systemd/journal:/run/systemd/journal \
-  -v /var/etw:/var/etw:z \
-  $RPIMAGE \
-  monitor
-Restart=always
-RestartSec=1
-StartLimitInterval=0
-
-[Install]
-WantedBy=multi-user.target
-EOF
-
-cat >/etc/sysconfig/aro-portal <<EOF
-AZURE_PORTAL_ACCESS_GROUP_IDS='$PORTALACCESSGROUPIDS'
-AZURE_PORTAL_CLIENT_ID='$PORTALCLIENTID'
-AZURE_PORTAL_ELEVATED_GROUP_IDS='$PORTALELEVATEDGROUPIDS'
-DATABASE_ACCOUNT_NAME='$DATABASEACCOUNTNAME'
-KEYVAULT_PREFIX='$KEYVAULTPREFIX'
-MDM_ACCOUNT=AzureRedHatOpenShiftRP
-MDM_NAMESPACE=Portal
-PORTAL_HOSTNAME='$LOCATION.admin.$RPPARENTDOMAINNAME'
-RP_MODE='$RPMODE'
-RPIMAGE='$RPIMAGE'
-EOF
-
-cat >/etc/systemd/system/aro-portal.service <<'EOF'
-[Unit]
-After=docker.service
-Requires=docker.service
-StartLimitInterval=0
-
-[Service]
-EnvironmentFile=/etc/sysconfig/aro-portal
-ExecStartPre=-/usr/bin/docker rm -f %N
-ExecStart=/usr/bin/docker run \
-  --hostname %H \
-  --name %N \
-  --rm \
-  -e ADMIN_API_CLIENT_CERT_COMMON_NAME \
-  -e AZURE_PORTAL_ACCESS_GROUP_IDS \
-  -e AZURE_PORTAL_CLIENT_ID \
-  -e AZURE_PORTAL_ELEVATED_GROUP_IDS \
-  -e DATABASE_ACCOUNT_NAME \
-  -e KEYVAULT_PREFIX \
-  -e MDM_ACCOUNT \
-  -e MDM_NAMESPACE \
-  -e PORTAL_HOSTNAME \
-  -e RP_MODE \
-  -m 2g \
-  -p 444:8444 \
-  -p 2222:2222 \
-  -v /run/systemd/journal:/run/systemd/journal \
-  -v /var/etw:/var/etw:z \
-  $RPIMAGE \
-  portal
-Restart=always
-RestartSec=1
-
-[Install]
-WantedBy=multi-user.target
-EOF
-
-chcon -R system_u:object_r:var_log_t:s0 /var/opt/microsoft/linuxmonagent
-
-mkdir -p /var/lib/waagent/Microsoft.Azure.KeyVault.Store
-
-for var in "mdsd" "mdm"; do
-cat >/etc/systemd/system/download-$var-credentials.service <<EOF
-[Unit]
-Description=Periodic $var credentials refresh
-
-[Service]
-Type=oneshot
-ExecStart=/usr/local/bin/download-credentials.sh $var
-EOF
-
-cat >/etc/systemd/system/download-$var-credentials.timer <<EOF
-[Unit]
-Description=Periodic $var credentials refresh
-
-[Timer]
-OnBootSec=0min
-OnCalendar=0/12:00:00
-AccuracySec=5s
-
-[Install]
-WantedBy=timers.target
-EOF
-done
-
-cat >/usr/local/bin/download-credentials.sh <<EOF
-#!/bin/bash
-set -eu
-
-COMPONENT="\$1"
-echo "Download \$COMPONENT credentials"
-
-TEMP_DIR=\$(mktemp -d)
-export AZURE_CONFIG_DIR=\$(mktemp -d)
-az login -i
-az account set -s "$SUBSCRIPTIONID"
-
-trap "cleanup" EXIT
-
-cleanup() {
-  az logout
-  [[ "\$TEMP_DIR" =~ /tmp/.+ ]] && rm -rf \$TEMP_DIR
-  [[ "\$AZURE_CONFIG_DIR" =~ /tmp/.+ ]] && rm -rf \$AZURE_CONFIG_DIR
-}
-
-if [ "\$COMPONENT" = "mdm" ]; then
-  CURRENT_CERT_FILE="/etc/mdm.pem"
-elif [ "\$COMPONENT" = "mdsd" ]; then
-  CURRENT_CERT_FILE="/var/lib/waagent/Microsoft.Azure.KeyVault.Store/mdsd.pem"
-else
-  echo Invalid usage && exit 1
-fi
-
-SECRET_NAME="rp-\${COMPONENT}"
-NEW_CERT_FILE="\$TEMP_DIR/\$COMPONENT.pem"
-for attempt in {1..5}; do
-  az keyvault secret download --file \$NEW_CERT_FILE --id "https://$KEYVAULTPREFIX-svc.vault.azure.net/secrets/\$SECRET_NAME" && break
-  if [[ \$attempt -lt 5 ]]; then sleep 10; else exit 1; fi
-done
-
-if [ -f \$NEW_CERT_FILE ]; then
-  if [ "\$COMPONENT" = "mdsd" ]; then
-    chown syslog:syslog \$NEW_CERT_FILE
-  else
-    sed -i -ne '1,/END CERTIFICATE/ p' \$NEW_CERT_FILE
-  fi
-  chmod 0600 \$NEW_CERT_FILE
-  mv \$NEW_CERT_FILE \$CURRENT_CERT_FILE
-else
-  echo Failed to refresh certificate for \$COMPONENT && exit 1
-fi
-EOF
-
-chmod u+x /usr/local/bin/download-credentials.sh
-
-systemctl enable download-mdsd-credentials.timer
-systemctl enable download-mdm-credentials.timer
-
-/usr/local/bin/download-credentials.sh mdsd
-/usr/local/bin/download-credentials.sh mdm
-MDSDCERTIFICATESAN=$(openssl x509 -in /var/lib/waagent/Microsoft.Azure.KeyVault.Store/mdsd.pem -noout -subject | sed -e 's/.*CN=//')
-
-mkdir /etc/systemd/system/mdsd.service.d
-cat >/etc/systemd/system/mdsd.service.d/override.conf <<'EOF'
-[Unit]
-After=network-online.target
-EOF
-
-cat >/etc/default/mdsd <<EOF
-MDSD_ROLE_PREFIX=/var/run/mdsd/default
-MDSD_OPTIONS="-A -d -r \$MDSD_ROLE_PREFIX"
-
-export MONITORING_GCS_ENVIRONMENT='$MDSDENVIRONMENT'
-export MONITORING_GCS_ACCOUNT=ARORPLogs
-export MONITORING_GCS_REGION='$LOCATION'
-export MONITORING_GCS_AUTH_ID_TYPE=AuthKeyVault
-export MONITORING_GCS_AUTH_ID='$MDSDCERTIFICATESAN'
-export MONITORING_GCS_NAMESPACE=ARORPLogs
-export MONITORING_CONFIG_VERSION='$MDSDCONFIGVERSION'
-export MONITORING_USE_GENEVA_CONFIG_SERVICE=true
-
-export MONITORING_TENANT='$LOCATION'
-export MONITORING_ROLE=rp
-export MONITORING_ROLE_INSTANCE='$(hostname)'
-EOF
-
-# setting MONITORING_GCS_AUTH_ID_TYPE=AuthKeyVault seems to have caused mdsd not
-# to honour SSL_CERT_FILE any more, heaven only knows why.
-mkdir -p /usr/lib/ssl/certs
-csplit -f /usr/lib/ssl/certs/cert- -b %03d.pem /etc/pki/tls/certs/ca-bundle.crt /^$/1 {*} >/dev/null
-c_rehash /usr/lib/ssl/certs
-
-for service in aro-monitor aro-portal aro-rp auoms azsecd azsecmond mdsd mdm chronyd td-agent-bit; do
-  systemctl enable $service.service
-done
-
-for scan in baseline clamav software; do
-  /usr/local/bin/azsecd config -s $scan -d P1D
-done
-
-(sleep 30; reboot) &
-`))
-
-	parts = append(parts, "'\n'", fmt.Sprintf("base64ToString('%s')", trailer))
-
-	script := fmt.Sprintf("[base64(concat(%s))]", strings.Join(parts, ","))
-
-	return &arm.Resource{
-		Resource: &mgmtcompute.VirtualMachineScaleSet{
-			Sku: &mgmtcompute.Sku{
-				Name:     to.StringPtr("[parameters('vmSize')]"),
-				Tier:     to.StringPtr("Standard"),
-				Capacity: to.Int64Ptr(3),
-			},
-			VirtualMachineScaleSetProperties: &mgmtcompute.VirtualMachineScaleSetProperties{
-				UpgradePolicy: &mgmtcompute.UpgradePolicy{
-					Mode: mgmtcompute.UpgradeModeManual,
-				},
-				VirtualMachineProfile: &mgmtcompute.VirtualMachineScaleSetVMProfile{
-					OsProfile: &mgmtcompute.VirtualMachineScaleSetOSProfile{
-						ComputerNamePrefix: to.StringPtr("[concat('rp-', parameters('vmssName'), '-')]"),
-						AdminUsername:      to.StringPtr("cloud-user"),
-						LinuxConfiguration: &mgmtcompute.LinuxConfiguration{
-							DisablePasswordAuthentication: to.BoolPtr(true),
-							SSH: &mgmtcompute.SSHConfiguration{
-								PublicKeys: &[]mgmtcompute.SSHPublicKey{
-									{
-										Path:    to.StringPtr("/home/cloud-user/.ssh/authorized_keys"),
-										KeyData: to.StringPtr("[parameters('sshPublicKey')]"),
-									},
-								},
-							},
-						},
-					},
-					StorageProfile: &mgmtcompute.VirtualMachineScaleSetStorageProfile{
-						ImageReference: &mgmtcompute.ImageReference{
-							Publisher: to.StringPtr("RedHat"),
-							Offer:     to.StringPtr("RHEL"),
-							Sku:       to.StringPtr("7-LVM"),
-							Version:   to.StringPtr("latest"),
-						},
-						OsDisk: &mgmtcompute.VirtualMachineScaleSetOSDisk{
-							CreateOption: mgmtcompute.DiskCreateOptionTypesFromImage,
-							ManagedDisk: &mgmtcompute.VirtualMachineScaleSetManagedDiskParameters{
-								StorageAccountType: mgmtcompute.StorageAccountTypesPremiumLRS,
-							},
-						},
-					},
-					NetworkProfile: &mgmtcompute.VirtualMachineScaleSetNetworkProfile{
-						HealthProbe: &mgmtcompute.APIEntityReference{
-							ID: to.StringPtr("[resourceId('Microsoft.Network/loadBalancers/probes', 'rp-lb', 'rp-probe')]"),
-						},
-						NetworkInterfaceConfigurations: &[]mgmtcompute.VirtualMachineScaleSetNetworkConfiguration{
-							{
-								Name: to.StringPtr("rp-vmss-nic"),
-								VirtualMachineScaleSetNetworkConfigurationProperties: &mgmtcompute.VirtualMachineScaleSetNetworkConfigurationProperties{
-									Primary: to.BoolPtr(true),
-									IPConfigurations: &[]mgmtcompute.VirtualMachineScaleSetIPConfiguration{
-										{
-											Name: to.StringPtr("rp-vmss-ipconfig"),
-											VirtualMachineScaleSetIPConfigurationProperties: &mgmtcompute.VirtualMachineScaleSetIPConfigurationProperties{
-												Subnet: &mgmtcompute.APIEntityReference{
-													ID: to.StringPtr("[resourceId('Microsoft.Network/virtualNetworks/subnets', 'rp-vnet', 'rp-subnet')]"),
-												},
-												Primary: to.BoolPtr(true),
-												PublicIPAddressConfiguration: &mgmtcompute.VirtualMachineScaleSetPublicIPAddressConfiguration{
-													Name: to.StringPtr("rp-vmss-pip"),
-												},
-												LoadBalancerBackendAddressPools: &[]mgmtcompute.SubResource{
-													{
-														ID: to.StringPtr("[resourceId('Microsoft.Network/loadBalancers/backendAddressPools', 'rp-lb', 'rp-backend')]"),
-													},
-												},
-											},
-										},
-									},
-								},
-							},
-						},
-					},
-					SecurityProfile: &mgmtcompute.SecurityProfile{
-						EncryptionAtHost: to.BoolPtr(true),
-					},
-					ExtensionProfile: &mgmtcompute.VirtualMachineScaleSetExtensionProfile{
-						Extensions: &[]mgmtcompute.VirtualMachineScaleSetExtension{
-							{
-								Name: to.StringPtr("rp-vmss-cse"),
-								VirtualMachineScaleSetExtensionProperties: &mgmtcompute.VirtualMachineScaleSetExtensionProperties{
-									Publisher:               to.StringPtr("Microsoft.Azure.Extensions"),
-									Type:                    to.StringPtr("CustomScript"),
-									TypeHandlerVersion:      to.StringPtr("2.0"),
-									AutoUpgradeMinorVersion: to.BoolPtr(true),
-									Settings:                map[string]interface{}{},
-									ProtectedSettings: map[string]interface{}{
-										"script": script,
-									},
-								},
-							},
-						},
-					},
-					DiagnosticsProfile: &mgmtcompute.DiagnosticsProfile{
-						BootDiagnostics: &mgmtcompute.BootDiagnostics{
-							Enabled:    to.BoolPtr(true),
-							StorageURI: to.StringPtr("[concat('https://', parameters('storageAccountDomain'), '/')]"),
-						},
-					},
-				},
-				Overprovision: to.BoolPtr(false),
-			},
-			Identity: &mgmtcompute.VirtualMachineScaleSetIdentity{
-				Type: mgmtcompute.ResourceIdentityTypeUserAssigned,
-				UserAssignedIdentities: map[string]*mgmtcompute.VirtualMachineScaleSetIdentityUserAssignedIdentitiesValue{
-					"[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', concat('aro-rp-', resourceGroup().location))]": {},
-				},
-			},
-			Name:     to.StringPtr("[concat('rp-vmss-', parameters('vmssName'))]"),
-			Type:     to.StringPtr("Microsoft.Compute/virtualMachineScaleSets"),
-			Location: to.StringPtr("[resourceGroup().location]"),
-		},
-		APIVersion: azureclient.APIVersion("Microsoft.Compute"),
-		DependsOn: []string{
-			"[resourceId('Microsoft.Authorization/roleAssignments', guid(resourceGroup().id, parameters('rpServicePrincipalId'), 'RP / Reader'))]",
-			"[resourceId('Microsoft.Network/virtualNetworks', 'rp-vnet')]",
-			"[resourceId('Microsoft.Network/loadBalancers', 'rp-lb')]",
-			"[resourceId('Microsoft.Storage/storageAccounts', substring(parameters('storageAccountDomain'), 0, indexOf(parameters('storageAccountDomain'), '.')))]",
-		},
-	}
-}
-
-func (g *generator) zone() *arm.Resource {
-	return &arm.Resource{
-		Resource: &mgmtdns.Zone{
-			ZoneProperties: &mgmtdns.ZoneProperties{},
-			Name:           to.StringPtr("[concat(resourceGroup().location, '.', parameters('clusterParentDomainName'))]"),
-			Type:           to.StringPtr("Microsoft.Network/dnsZones"),
-			Location:       to.StringPtr("global"),
-		},
-		APIVersion: azureclient.APIVersion("Microsoft.Network/dnsZones"),
-	}
-}
-
-func (g *generator) clusterKeyvaultAccessPolicies() []mgmtkeyvault.AccessPolicyEntry {
-	return []mgmtkeyvault.AccessPolicyEntry{
-		// TODO: remove fpServicePrincipalId after next RP rollout
-		{
-			TenantID: &tenantUUIDHack,
-			ObjectID: to.StringPtr("[parameters('fpServicePrincipalId')]"),
-			Permissions: &mgmtkeyvault.Permissions{
-				Secrets: &[]mgmtkeyvault.SecretPermissions{
-					mgmtkeyvault.SecretPermissionsGet,
-				},
-				Certificates: &[]mgmtkeyvault.CertificatePermissions{
-					mgmtkeyvault.Create,
-					mgmtkeyvault.Delete,
-					mgmtkeyvault.Get,
-					mgmtkeyvault.Update,
-				},
-			},
-		},
-		{
-			TenantID: &tenantUUIDHack,
-			ObjectID: to.StringPtr("[parameters('rpServicePrincipalId')]"),
-			Permissions: &mgmtkeyvault.Permissions{
-				Secrets: &[]mgmtkeyvault.SecretPermissions{
-					mgmtkeyvault.SecretPermissionsGet,
-				},
-				Certificates: &[]mgmtkeyvault.CertificatePermissions{
-					mgmtkeyvault.Create,
-					mgmtkeyvault.Delete,
-					mgmtkeyvault.Get,
-					mgmtkeyvault.Update,
-				},
-			},
-		},
-	}
-}
-
-func (g *generator) portalKeyvaultAccessPolicies() []mgmtkeyvault.AccessPolicyEntry {
-	return []mgmtkeyvault.AccessPolicyEntry{
-		{
-			TenantID: &tenantUUIDHack,
-			ObjectID: to.StringPtr("[parameters('rpServicePrincipalId')]"),
-			Permissions: &mgmtkeyvault.Permissions{
-				Secrets: &[]mgmtkeyvault.SecretPermissions{
-					mgmtkeyvault.SecretPermissionsGet,
-				},
-			},
-		},
-	}
-}
-
-func (g *generator) serviceKeyvaultAccessPolicies() []mgmtkeyvault.AccessPolicyEntry {
-	return []mgmtkeyvault.AccessPolicyEntry{
-		{
-			TenantID: &tenantUUIDHack,
-			ObjectID: to.StringPtr("[parameters('rpServicePrincipalId')]"),
-			Permissions: &mgmtkeyvault.Permissions{
-				Secrets: &[]mgmtkeyvault.SecretPermissions{
-					mgmtkeyvault.SecretPermissionsGet,
-				},
-			},
-		},
-	}
-}
-
-func (g *generator) clusterKeyvault() *arm.Resource {
-	vault := &mgmtkeyvault.Vault{
-		Properties: &mgmtkeyvault.VaultProperties{
-			EnableSoftDelete: to.BoolPtr(true),
-			TenantID:         &tenantUUIDHack,
-			Sku: &mgmtkeyvault.Sku{
-				Name:   mgmtkeyvault.Standard,
-				Family: to.StringPtr("A"),
-			},
-			AccessPolicies: &[]mgmtkeyvault.AccessPolicyEntry{},
-		},
-		Name:     to.StringPtr("[concat(parameters('keyvaultPrefix'), '" + env.ClusterKeyvaultSuffix + "')]"),
-		Type:     to.StringPtr("Microsoft.KeyVault/vaults"),
-		Location: to.StringPtr("[resourceGroup().location]"),
-	}
-
-	if !g.production {
-		*vault.Properties.AccessPolicies = append(g.clusterKeyvaultAccessPolicies(),
-			mgmtkeyvault.AccessPolicyEntry{
-				TenantID: &tenantUUIDHack,
-				ObjectID: to.StringPtr("[parameters('adminObjectId')]"),
-				Permissions: &mgmtkeyvault.Permissions{
-					Certificates: &[]mgmtkeyvault.CertificatePermissions{
-						mgmtkeyvault.Get,
-						mgmtkeyvault.List,
-					},
-				},
-			},
-		)
-	}
-
-	return &arm.Resource{
-		Resource:   vault,
-		APIVersion: azureclient.APIVersion("Microsoft.KeyVault"),
-	}
-}
-
-func (g *generator) portalKeyvault() *arm.Resource {
-	vault := &mgmtkeyvault.Vault{
-		Properties: &mgmtkeyvault.VaultProperties{
-			EnableSoftDelete: to.BoolPtr(true),
-			TenantID:         &tenantUUIDHack,
-			Sku: &mgmtkeyvault.Sku{
-				Name:   mgmtkeyvault.Standard,
-				Family: to.StringPtr("A"),
-			},
-			AccessPolicies: &[]mgmtkeyvault.AccessPolicyEntry{},
-		},
-		Name:     to.StringPtr("[concat(parameters('keyvaultPrefix'), '" + env.PortalKeyvaultSuffix + "')]"),
-		Type:     to.StringPtr("Microsoft.KeyVault/vaults"),
-		Location: to.StringPtr("[resourceGroup().location]"),
-	}
-
-	if !g.production {
-		*vault.Properties.AccessPolicies = append(g.portalKeyvaultAccessPolicies(),
-			mgmtkeyvault.AccessPolicyEntry{
-				TenantID: &tenantUUIDHack,
-				ObjectID: to.StringPtr("[parameters('adminObjectId')]"),
-				Permissions: &mgmtkeyvault.Permissions{
-					Certificates: &[]mgmtkeyvault.CertificatePermissions{
-						mgmtkeyvault.Delete,
-						mgmtkeyvault.Get,
-						mgmtkeyvault.Import,
-						mgmtkeyvault.List,
-					},
-					Secrets: &[]mgmtkeyvault.SecretPermissions{
-						mgmtkeyvault.SecretPermissionsSet,
-						mgmtkeyvault.SecretPermissionsList,
-					},
-				},
-			},
-		)
-	}
-
-	return &arm.Resource{
-		Resource:   vault,
-		APIVersion: azureclient.APIVersion("Microsoft.KeyVault"),
-	}
-}
-
-func (g *generator) serviceKeyvault() *arm.Resource {
-	vault := &mgmtkeyvault.Vault{
-		Properties: &mgmtkeyvault.VaultProperties{
-			EnableSoftDelete: to.BoolPtr(true),
-			TenantID:         &tenantUUIDHack,
-			Sku: &mgmtkeyvault.Sku{
-				Name:   mgmtkeyvault.Standard,
-				Family: to.StringPtr("A"),
-			},
-			AccessPolicies: &[]mgmtkeyvault.AccessPolicyEntry{},
-		},
-		Name:     to.StringPtr("[concat(parameters('keyvaultPrefix'), '" + env.ServiceKeyvaultSuffix + "')]"),
-		Type:     to.StringPtr("Microsoft.KeyVault/vaults"),
-		Location: to.StringPtr("[resourceGroup().location]"),
-	}
-
-	if !g.production {
-		*vault.Properties.AccessPolicies = append(g.serviceKeyvaultAccessPolicies(),
-			mgmtkeyvault.AccessPolicyEntry{
-				TenantID: &tenantUUIDHack,
-				ObjectID: to.StringPtr("[parameters('adminObjectId')]"),
-				Permissions: &mgmtkeyvault.Permissions{
-					Certificates: &[]mgmtkeyvault.CertificatePermissions{
-						mgmtkeyvault.Delete,
-						mgmtkeyvault.Get,
-						mgmtkeyvault.Import,
-						mgmtkeyvault.List,
-					},
-					Secrets: &[]mgmtkeyvault.SecretPermissions{
-						mgmtkeyvault.SecretPermissionsSet,
-						mgmtkeyvault.SecretPermissionsList,
-					},
-				},
-			},
-		)
-	}
-
-	return &arm.Resource{
-		Resource:   vault,
-		APIVersion: azureclient.APIVersion("Microsoft.KeyVault"),
-	}
-}
-
-func (g *generator) cosmosdb() []*arm.Resource {
-	cosmosdb := &mgmtdocumentdb.DatabaseAccountCreateUpdateParameters{
-		Kind: mgmtdocumentdb.GlobalDocumentDB,
-		DatabaseAccountCreateUpdateProperties: &mgmtdocumentdb.DatabaseAccountCreateUpdateProperties{
-			ConsistencyPolicy: &mgmtdocumentdb.ConsistencyPolicy{
-				DefaultConsistencyLevel: mgmtdocumentdb.Strong,
-			},
-			Locations: &[]mgmtdocumentdb.Location{
-				{
-					LocationName: to.StringPtr("[resourceGroup().location]"),
-				},
-			},
-			DatabaseAccountOfferType: to.StringPtr(string(mgmtdocumentdb.Standard)),
-		},
-		Name:     to.StringPtr("[parameters('databaseAccountName')]"),
-		Type:     to.StringPtr("Microsoft.DocumentDB/databaseAccounts"),
-		Location: to.StringPtr("[resourceGroup().location]"),
-		Tags: map[string]*string{
-			"defaultExperience": to.StringPtr("Core (SQL)"),
-		},
-	}
-
-	r := &arm.Resource{
-		Resource:   cosmosdb,
-		APIVersion: azureclient.APIVersion("Microsoft.DocumentDB"),
-	}
-
-	if g.production {
-		cosmosdb.IPRangeFilter = to.StringPtr("[concat('104.42.195.92,40.76.54.131,52.176.6.30,52.169.50.45,52.187.184.26', if(equals(parameters('extraCosmosDBIPs'), ''), '', ','), parameters('extraCosmosDBIPs'))]")
-		cosmosdb.IsVirtualNetworkFilterEnabled = to.BoolPtr(true)
-		cosmosdb.VirtualNetworkRules = &[]mgmtdocumentdb.VirtualNetworkRule{
-			{
-				ID: to.StringPtr("[resourceId('Microsoft.Network/virtualNetworks/subnets', 'rp-vnet', 'rp-subnet')]"),
-			},
-		}
-		cosmosdb.DisableKeyBasedMetadataWriteAccess = to.BoolPtr(true)
-
-		r.DependsOn = append(r.DependsOn, "[resourceId('Microsoft.Network/virtualNetworks', 'rp-vnet')]")
-	}
-
-	rs := []*arm.Resource{
-		r,
-	}
-
-	if g.production {
-		rs = append(rs, g.database("'ARO'", true)...)
-	}
-
-	return rs
-}
-
-func (g *generator) database(databaseName string, addDependsOn bool) []*arm.Resource {
-	portal := &arm.Resource{
-		Resource: &mgmtdocumentdb.SQLContainerCreateUpdateParameters{
-			SQLContainerCreateUpdateProperties: &mgmtdocumentdb.SQLContainerCreateUpdateProperties{
-				Resource: &mgmtdocumentdb.SQLContainerResource{
-					ID: to.StringPtr("Portal"),
-					PartitionKey: &mgmtdocumentdb.ContainerPartitionKey{
-						Paths: &[]string{
-							"/id",
-						},
-						Kind: mgmtdocumentdb.PartitionKindHash,
-					},
-					DefaultTTL: to.Int32Ptr(-1),
-				},
-				Options: map[string]*string{},
-			},
-			Name:     to.StringPtr("[concat(parameters('databaseAccountName'), '/', " + databaseName + ", '/Portal')]"),
-			Type:     to.StringPtr("Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers"),
-			Location: to.StringPtr("[resourceGroup().location]"),
-		},
-		APIVersion: azureclient.APIVersion("Microsoft.DocumentDB"),
-		DependsOn: []string{
-			"[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlDatabases', parameters('databaseAccountName'), " + databaseName + ")]",
-		},
-	}
-
-	if g.production {
-		portal.Resource.(*mgmtdocumentdb.SQLContainerCreateUpdateParameters).SQLContainerCreateUpdateProperties.Options["throughput"] = to.StringPtr("400")
-	}
-
-	rs := []*arm.Resource{
-		{
-			Resource: &mgmtdocumentdb.SQLDatabaseCreateUpdateParameters{
-				SQLDatabaseCreateUpdateProperties: &mgmtdocumentdb.SQLDatabaseCreateUpdateProperties{
-					Resource: &mgmtdocumentdb.SQLDatabaseResource{
-						ID: to.StringPtr("[" + databaseName + "]"),
-					},
-					Options: map[string]*string{
-						"throughput": to.StringPtr("500"),
-					},
-				},
-				Name:     to.StringPtr("[concat(parameters('databaseAccountName'), '/', " + databaseName + ")]"),
-				Type:     to.StringPtr("Microsoft.DocumentDB/databaseAccounts/sqlDatabases"),
-				Location: to.StringPtr("[resourceGroup().location]"),
-			},
-			APIVersion: azureclient.APIVersion("Microsoft.DocumentDB"),
-		},
-		{
-			Resource: &mgmtdocumentdb.SQLContainerCreateUpdateParameters{
-				SQLContainerCreateUpdateProperties: &mgmtdocumentdb.SQLContainerCreateUpdateProperties{
-					Resource: &mgmtdocumentdb.SQLContainerResource{
-						ID: to.StringPtr("AsyncOperations"),
-						PartitionKey: &mgmtdocumentdb.ContainerPartitionKey{
-							Paths: &[]string{
-								"/id",
-							},
-							Kind: mgmtdocumentdb.PartitionKindHash,
-						},
-						DefaultTTL: to.Int32Ptr(7 * 86400), // 7 days
-					},
-					Options: map[string]*string{},
-				},
-				Name:     to.StringPtr("[concat(parameters('databaseAccountName'), '/', " + databaseName + ", '/AsyncOperations')]"),
-				Type:     to.StringPtr("Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers"),
-				Location: to.StringPtr("[resourceGroup().location]"),
-			},
-			APIVersion: azureclient.APIVersion("Microsoft.DocumentDB"),
-			DependsOn: []string{
-				"[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlDatabases', parameters('databaseAccountName'), " + databaseName + ")]",
-			},
-		},
-		{
-			Resource: &mgmtdocumentdb.SQLContainerCreateUpdateParameters{
-				SQLContainerCreateUpdateProperties: &mgmtdocumentdb.SQLContainerCreateUpdateProperties{
-					Resource: &mgmtdocumentdb.SQLContainerResource{
-						ID: to.StringPtr("Billing"),
-						PartitionKey: &mgmtdocumentdb.ContainerPartitionKey{
-							Paths: &[]string{
-								"/id",
-							},
-							Kind: mgmtdocumentdb.PartitionKindHash,
-						},
-					},
-					Options: map[string]*string{},
-				},
-				Name:     to.StringPtr("[concat(parameters('databaseAccountName'), '/', " + databaseName + ", '/Billing')]"),
-				Type:     to.StringPtr("Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers"),
-				Location: to.StringPtr("[resourceGroup().location]"),
-			},
-			APIVersion: azureclient.APIVersion("Microsoft.DocumentDB"),
-			DependsOn: []string{
-				"[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlDatabases', parameters('databaseAccountName'), " + databaseName + ")]",
-			},
-		},
-		{
-			Resource: &mgmtdocumentdb.SQLContainerCreateUpdateParameters{
-				SQLContainerCreateUpdateProperties: &mgmtdocumentdb.SQLContainerCreateUpdateProperties{
-					Resource: &mgmtdocumentdb.SQLContainerResource{
-						ID: to.StringPtr("Monitors"),
-						PartitionKey: &mgmtdocumentdb.ContainerPartitionKey{
-							Paths: &[]string{
-								"/id",
-							},
-							Kind: mgmtdocumentdb.PartitionKindHash,
-						},
-						DefaultTTL: to.Int32Ptr(-1),
-					},
-					Options: map[string]*string{},
-				},
-				Name:     to.StringPtr("[concat(parameters('databaseAccountName'), '/', " + databaseName + ", '/Monitors')]"),
-				Type:     to.StringPtr("Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers"),
-				Location: to.StringPtr("[resourceGroup().location]"),
-			},
-			APIVersion: azureclient.APIVersion("Microsoft.DocumentDB"),
-			DependsOn: []string{
-				"[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlDatabases', parameters('databaseAccountName'), " + databaseName + ")]",
-			},
-		},
-		{
-			Resource: &mgmtdocumentdb.SQLContainerCreateUpdateParameters{
-				SQLContainerCreateUpdateProperties: &mgmtdocumentdb.SQLContainerCreateUpdateProperties{
-					Resource: &mgmtdocumentdb.SQLContainerResource{
-						ID: to.StringPtr("OpenShiftClusters"),
-						PartitionKey: &mgmtdocumentdb.ContainerPartitionKey{
-							Paths: &[]string{
-								"/partitionKey",
-							},
-							Kind: mgmtdocumentdb.PartitionKindHash,
-						},
-						UniqueKeyPolicy: &mgmtdocumentdb.UniqueKeyPolicy{
-							UniqueKeys: &[]mgmtdocumentdb.UniqueKey{
-								{
-									Paths: &[]string{
-										"/key",
-									},
-								},
-								{
-									Paths: &[]string{
-										"/clusterResourceGroupIdKey",
-									},
-								},
-								{
-									Paths: &[]string{
-										"/clientIdKey",
-									},
-								},
-							},
-						},
-					},
-					Options: map[string]*string{},
-				},
-				Name:     to.StringPtr("[concat(parameters('databaseAccountName'), '/', " + databaseName + ", '/OpenShiftClusters')]"),
-				Type:     to.StringPtr("Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers"),
-				Location: to.StringPtr("[resourceGroup().location]"),
-			},
-			APIVersion: azureclient.APIVersion("Microsoft.DocumentDB"),
-			DependsOn: []string{
-				"[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlDatabases', parameters('databaseAccountName'), " + databaseName + ")]",
-			},
-		},
-		portal,
-		{
-			Resource: &mgmtdocumentdb.SQLContainerCreateUpdateParameters{
-				SQLContainerCreateUpdateProperties: &mgmtdocumentdb.SQLContainerCreateUpdateProperties{
-					Resource: &mgmtdocumentdb.SQLContainerResource{
-						ID: to.StringPtr("Subscriptions"),
-						PartitionKey: &mgmtdocumentdb.ContainerPartitionKey{
-							Paths: &[]string{
-								"/id",
-							},
-							Kind: mgmtdocumentdb.PartitionKindHash,
-						},
-					},
-					Options: map[string]*string{},
-				},
-				Name:     to.StringPtr("[concat(parameters('databaseAccountName'), '/', " + databaseName + ", '/Subscriptions')]"),
-				Type:     to.StringPtr("Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers"),
-				Location: to.StringPtr("[resourceGroup().location]"),
-			},
-			APIVersion: azureclient.APIVersion("Microsoft.DocumentDB"),
-			DependsOn: []string{
-				"[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlDatabases', parameters('databaseAccountName'), " + databaseName + ")]",
-			},
-		},
-	}
-
-	if addDependsOn {
-		for i := range rs {
-			rs[i].DependsOn = append(rs[i].DependsOn,
-				"[resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName'))]",
-			)
-		}
-	}
-
-	return rs
-}
-
-func (g *generator) roleDefinitionTokenContributor() *arm.Resource {
-	return &arm.Resource{
-		Resource: &mgmtauthorization.RoleDefinition{
-			Name: to.StringPtr("48983534-3d06-4dcb-a566-08a694eb1279"),
-			Type: to.StringPtr("Microsoft.Authorization/roleDefinitions"),
-			RoleDefinitionProperties: &mgmtauthorization.RoleDefinitionProperties{
-				RoleName:         to.StringPtr("ARO v4 ContainerRegistry Token Contributor"),
-				AssignableScopes: &[]string{"[subscription().id]"},
-				Permissions: &[]mgmtauthorization.Permission{
-					{
-						Actions: &[]string{
-							"Microsoft.ContainerRegistry/registries/generateCredentials/action",
-							"Microsoft.ContainerRegistry/registries/scopeMaps/read",
-							"Microsoft.ContainerRegistry/registries/tokens/delete",
-							"Microsoft.ContainerRegistry/registries/tokens/operationStatuses/read",
-							"Microsoft.ContainerRegistry/registries/tokens/read",
-							"Microsoft.ContainerRegistry/registries/tokens/write",
-						},
-					},
-				},
-			},
-		},
-		APIVersion: azureclient.APIVersion("Microsoft.Authorization/roleDefinitions"),
-	}
-}
-
-func (g *generator) rbac() []*arm.Resource {
-	return []*arm.Resource{
-		rbac.ResourceGroupRoleAssignmentWithName(
-			rbac.RoleReader,
-			"parameters('rpServicePrincipalId')",
-			"guid(resourceGroup().id, parameters('rpServicePrincipalId'), 'RP / Reader')",
-		),
-		rbac.ResourceGroupRoleAssignmentWithName(
-			rbac.RoleNetworkContributor,
-			"parameters('fpServicePrincipalId')",
-			"guid(resourceGroup().id, 'FP / Network Contributor')",
-		),
-		rbac.ResourceRoleAssignmentWithName(
-			rbac.RoleDocumentDBAccountContributor,
-			"parameters('rpServicePrincipalId')",
-			"Microsoft.DocumentDB/databaseAccounts",
-			"parameters('databaseAccountName')",
-			"concat(parameters('databaseAccountName'), '/Microsoft.Authorization/', guid(resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName')), parameters('rpServicePrincipalId'), 'RP / DocumentDB Account Contributor'))",
-		),
-		rbac.ResourceRoleAssignmentWithName(
-			rbac.RoleDNSZoneContributor,
-			"parameters('fpServicePrincipalId')",
-			"Microsoft.Network/dnsZones",
-			"concat(resourceGroup().location, '.', parameters('clusterParentDomainName'))",
-			"concat(resourceGroup().location, '.', parameters('clusterParentDomainName'), '/Microsoft.Authorization/', guid(resourceId('Microsoft.Network/dnsZones', concat(resourceGroup().location, '.', parameters('clusterParentDomainName'))), 'FP / DNS Zone Contributor'))",
-		),
-	}
-}
-
-func (g *generator) billingContributorRbac() []*arm.Resource {
-	return []*arm.Resource{
-		rbac.ResourceRoleAssignmentWithName(
-			rbac.RoleDocumentDBAccountContributor,
-			"parameters('billingServicePrincipalId')",
-			"Microsoft.DocumentDB/databaseAccounts",
-			"parameters('databaseAccountName')",
-			"concat(parameters('databaseAccountName'), '/Microsoft.Authorization/', guid(resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName')), parameters('billingServicePrincipalId') , 'Billing / DocumentDB Account Contributor'))",
-		),
-	}
-}
-
-func (g *generator) acrReplica() *arm.Resource {
-	return &arm.Resource{
-		Resource: &mgmtcontainerregistry.Replication{
-			Name:     to.StringPtr("[concat(substring(parameters('acrResourceId'), add(lastIndexOf(parameters('acrResourceId'), '/'), 1)), '/', parameters('location'))]"),
-			Type:     to.StringPtr("Microsoft.ContainerRegistry/registries/replications"),
-			Location: to.StringPtr("[parameters('location')]"),
-		},
-		APIVersion: azureclient.APIVersion("Microsoft.ContainerRegistry"),
-	}
-}
-
-func (g *generator) acrRbac() []*arm.Resource {
-	rs := []*arm.Resource{
-		rbac.ResourceRoleAssignmentWithName(
-			rbac.RoleACRPull,
-			"parameters('rpServicePrincipalId')",
-			"Microsoft.ContainerRegistry/registries",
-			"substring(parameters('acrResourceId'), add(lastIndexOf(parameters('acrResourceId'), '/'), 1))",
-			"concat(substring(parameters('acrResourceId'), add(lastIndexOf(parameters('acrResourceId'), '/'), 1)), '/', '/Microsoft.Authorization/', guid(concat(parameters('acrResourceId'), parameters('rpServicePrincipalId'), 'RP / AcrPull')))",
-		),
-		rbac.ResourceRoleAssignmentWithName(
-			"48983534-3d06-4dcb-a566-08a694eb1279", // ARO v4 ContainerRegistry Token Contributor
-			"parameters('fpServicePrincipalId')",
-			"Microsoft.ContainerRegistry/registries",
-			"substring(parameters('acrResourceId'), add(lastIndexOf(parameters('acrResourceId'), '/'), 1))",
-			"concat(substring(parameters('acrResourceId'), add(lastIndexOf(parameters('acrResourceId'), '/'), 1)), '/', '/Microsoft.Authorization/', guid(concat(parameters('acrResourceId'), 'FP / ARO v4 ContainerRegistry Token Contributor')))",
-		),
-	}
-
-	for _, r := range rs {
-		r.DependsOn = nil
-	}
-
-	return rs
-}
-
-func (g *generator) rpVersionStorageAccount() []*arm.Resource {
-	return []*arm.Resource{
-		{
-			Resource: &mgmtstorage.Account{
-				Name:     to.StringPtr("[parameters('rpVersionStorageAccountName')]"),
-				Type:     to.StringPtr("Microsoft.Storage/storageAccounts"),
-				Location: to.StringPtr("[resourceGroup().location]"),
-				AccountProperties: &mgmtstorage.AccountProperties{
-					AllowBlobPublicAccess: to.BoolPtr(true),
-				},
-				Sku: &mgmtstorage.Sku{
-					Name: "Standard_LRS",
-				},
-			},
-			APIVersion: azureclient.APIVersion("Microsoft.Storage"),
-		},
-		{
-			Resource: &mgmtstorage.BlobContainer{
-				Name: to.StringPtr("[concat(parameters('rpVersionStorageAccountName'), '/default/rpversion')]"),
-				Type: to.StringPtr("Microsoft.Storage/storageAccounts/blobServices/containers"),
-				ContainerProperties: &mgmtstorage.ContainerProperties{
-					PublicAccess: mgmtstorage.PublicAccessContainer,
-				},
-			},
-			APIVersion: azureclient.APIVersion("Microsoft.Storage"),
-			DependsOn: []string{
-				"[resourceId('Microsoft.Storage/storageAccounts', parameters('rpVersionStorageAccountName'))]",
-			},
-		},
-	}
-}
-
-func (g *generator) storageAccount() *arm.Resource {
-	return &arm.Resource{
-		Resource: &mgmtstorage.Account{
-			Name:     to.StringPtr("[substring(parameters('storageAccountDomain'), 0, indexOf(parameters('storageAccountDomain'), '.'))]"),
-			Type:     to.StringPtr("Microsoft.Storage/storageAccounts"),
-			Location: to.StringPtr("[resourceGroup().location]"),
-			Sku: &mgmtstorage.Sku{
-				Name: "Standard_LRS",
-			},
-		},
-		APIVersion: azureclient.APIVersion("Microsoft.Storage"),
-	}
-}
-
-func (g *generator) devCIPool() *arm.Resource {
-	parts := []string{
-		fmt.Sprintf("base64ToString('%s')", base64.StdEncoding.EncodeToString([]byte("set -e\n\n"))),
-	}
-
-	for _, variable := range []string{
-		"ciAzpToken",
-		"ciPoolName"} {
-		parts = append(parts,
-			fmt.Sprintf("'%s='''", strings.ToUpper(variable)),
-			fmt.Sprintf("parameters('%s')", variable),
-			"'''\n'",
-		)
-	}
-
-	trailer := base64.StdEncoding.EncodeToString([]byte(`
-yum -y update -x WALinuxAgent
-
-lvextend -l +50%FREE /dev/rootvg/varlv
-xfs_growfs /var
-
-lvextend -l +100%FREE /dev/rootvg/homelv
-xfs_growfs /home
-
-rpm --import https://dl.fedoraproject.org/pub/epel/RPM-GPG-KEY-EPEL-7
-rpm --import https://packages.microsoft.com/keys/microsoft.asc
-
-yum -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
-
-cat >/etc/yum.repos.d/azure.repo <<'EOF'
-[azure-cli]
-name=azure-cli
-baseurl=https://packages.microsoft.com/yumrepos/azure-cli
-enabled=yes
-gpgcheck=yes
-EOF
-
-yum --enablerepo=rhui-rhel-7-server-rhui-optional-rpms -y install azure-cli docker jq libassuan-devel gcc gpgme-devel rh-git29 rh-python36 tmpwatch lttng-usr
-
-GO_VERSION=1.14.9
-curl https://dl.google.com/go/go${GO_VERSION}.linux-amd64.tar.gz | tar -C /usr/local -xz
-ln -s /usr/local/go/bin/* /usr/local/bin
-
-VSTS_AGENT_VERSION=2.181.1
-mkdir /home/cloud-user/agent
-pushd /home/cloud-user/agent
-curl https://vstsagentpackage.azureedge.net/agent/${VSTS_AGENT_VERSION}/vsts-agent-linux-x64-${VSTS_AGENT_VERSION}.tar.gz | tar -xz
-chown -R cloud-user:cloud-user .
-
-./bin/installdependencies.sh
-sudo -u cloud-user ./config.sh --unattended --url https://dev.azure.com/msazure --auth pat --token "$CIAZPTOKEN" --pool "$CIPOOLNAME" --agent "ARO-RHEL-$HOSTNAME" --replace
-./svc.sh install cloud-user
-popd
-
-# merge in /opt/rh/rh-*/enable
-cat >/home/cloud-user/agent/.path <<'EOF'
-/opt/rh/rh-python36/root/usr/bin:/opt/rh/rh-git29/root/usr/bin:/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/cloud-user/.local/bin:/home/cloud-user/bin
-EOF
-
-cat >/home/cloud-user/agent/.env <<'EOF'
-LD_LIBRARY_PATH=/opt/rh/rh-python36/root/usr/lib64:/opt/rh/httpd24/root/usr/lib64
-MANPATH=/opt/rh/rh-python36/root/usr/share/man:/opt/rh/rh-git29/root/usr/share/man
-PERL5LIB=/opt/rh/rh-git29/root/usr/share/perl5/vendor_perl
-PKG_CONFIG_PATH=/opt/rh/rh-python36/root/usr/lib64/pkgconfig
-XDG_DATA_DIRS=/opt/rh/rh-python36/root/usr/share:/usr/local/share:/usr/share
-EOF
-
-sed -i -e 's/^OPTIONS='\''/OPTIONS='\''-G cloud-user /' /etc/sysconfig/docker
-
-systemctl enable docker
-
-cat >/etc/cron.hourly/tmpwatch <<'EOF'
-#!/bin/bash
-
-exec /sbin/tmpwatch 24h /tmp
-EOF
-chmod +x /etc/cron.hourly/tmpwatch
-
-(sleep 30; reboot) &
-`))
-
-	parts = append(parts, "'\n'", fmt.Sprintf("base64ToString('%s')", trailer))
-
-	script := fmt.Sprintf("[base64(concat(%s))]", strings.Join(parts, ","))
-
-	return &arm.Resource{
-		Resource: &mgmtcompute.VirtualMachineScaleSet{
-			Sku: &mgmtcompute.Sku{
-				Name:     to.StringPtr(string(mgmtcompute.VirtualMachineSizeTypesStandardD2sV3)),
-				Tier:     to.StringPtr("Standard"),
-				Capacity: to.Int64Ptr(1337),
-			},
-			VirtualMachineScaleSetProperties: &mgmtcompute.VirtualMachineScaleSetProperties{
-				UpgradePolicy: &mgmtcompute.UpgradePolicy{
-					Mode: mgmtcompute.UpgradeModeManual,
-				},
-				VirtualMachineProfile: &mgmtcompute.VirtualMachineScaleSetVMProfile{
-					OsProfile: &mgmtcompute.VirtualMachineScaleSetOSProfile{
-						ComputerNamePrefix: to.StringPtr("ci-"),
-						AdminUsername:      to.StringPtr("cloud-user"),
-						LinuxConfiguration: &mgmtcompute.LinuxConfiguration{
-							DisablePasswordAuthentication: to.BoolPtr(true),
-							SSH: &mgmtcompute.SSHConfiguration{
-								PublicKeys: &[]mgmtcompute.SSHPublicKey{
-									{
-										Path:    to.StringPtr("/home/cloud-user/.ssh/authorized_keys"),
-										KeyData: to.StringPtr("[parameters('sshPublicKey')]"),
-									},
-								},
-							},
-						},
-					},
-					StorageProfile: &mgmtcompute.VirtualMachineScaleSetStorageProfile{
-						ImageReference: &mgmtcompute.ImageReference{
-							Publisher: to.StringPtr("RedHat"),
-							Offer:     to.StringPtr("RHEL"),
-							Sku:       to.StringPtr("7-LVM"),
-							Version:   to.StringPtr("latest"),
-						},
-						OsDisk: &mgmtcompute.VirtualMachineScaleSetOSDisk{
-							CreateOption: mgmtcompute.DiskCreateOptionTypesFromImage,
-							ManagedDisk: &mgmtcompute.VirtualMachineScaleSetManagedDiskParameters{
-								StorageAccountType: mgmtcompute.StorageAccountTypesPremiumLRS,
-							},
-						},
-					},
-					NetworkProfile: &mgmtcompute.VirtualMachineScaleSetNetworkProfile{
-						NetworkInterfaceConfigurations: &[]mgmtcompute.VirtualMachineScaleSetNetworkConfiguration{
-							{
-								Name: to.StringPtr("ci-vmss-nic"),
-								VirtualMachineScaleSetNetworkConfigurationProperties: &mgmtcompute.VirtualMachineScaleSetNetworkConfigurationProperties{
-									Primary: to.BoolPtr(true),
-									IPConfigurations: &[]mgmtcompute.VirtualMachineScaleSetIPConfiguration{
-										{
-											Name: to.StringPtr("ci-vmss-ipconfig"),
-											VirtualMachineScaleSetIPConfigurationProperties: &mgmtcompute.VirtualMachineScaleSetIPConfigurationProperties{
-												Subnet: &mgmtcompute.APIEntityReference{
-													ID: to.StringPtr("[resourceId('Microsoft.Network/virtualNetworks/subnets', 'dev-vnet', 'ToolingSubnet')]"),
-												},
-												Primary: to.BoolPtr(true),
-												PublicIPAddressConfiguration: &mgmtcompute.VirtualMachineScaleSetPublicIPAddressConfiguration{
-													Name: to.StringPtr("ci-vmss-pip"),
-													VirtualMachineScaleSetPublicIPAddressConfigurationProperties: &mgmtcompute.VirtualMachineScaleSetPublicIPAddressConfigurationProperties{
-														DNSSettings: &mgmtcompute.VirtualMachineScaleSetPublicIPAddressConfigurationDNSSettings{
-															DomainNameLabel: to.StringPtr("aro-ci"),
-														},
-													},
-												},
-											},
-										},
-									},
-								},
-							},
-						},
-					},
-					ExtensionProfile: &mgmtcompute.VirtualMachineScaleSetExtensionProfile{
-						Extensions: &[]mgmtcompute.VirtualMachineScaleSetExtension{
-							{
-								Name: to.StringPtr("ci-vmss-cse"),
-								VirtualMachineScaleSetExtensionProperties: &mgmtcompute.VirtualMachineScaleSetExtensionProperties{
-									Publisher:               to.StringPtr("Microsoft.Azure.Extensions"),
-									Type:                    to.StringPtr("CustomScript"),
-									TypeHandlerVersion:      to.StringPtr("2.0"),
-									AutoUpgradeMinorVersion: to.BoolPtr(true),
-									Settings:                map[string]interface{}{},
-									ProtectedSettings: map[string]interface{}{
-										"script": script,
-									},
-								},
-							},
-						},
-					},
-				},
-				Overprovision: to.BoolPtr(false),
-			},
-			Name:     to.StringPtr("ci-vmss"),
-			Type:     to.StringPtr("Microsoft.Compute/virtualMachineScaleSets"),
-			Location: to.StringPtr("[resourceGroup().location]"),
-		},
-		APIVersion: azureclient.APIVersion("Microsoft.Compute"),
-		Condition:  "[greater(parameters('ciCapacity'), 0)]", // TODO(mj): Refactor g.conditionStanza for better usage
-		DependsOn: []string{
-			"[resourceId('Microsoft.Network/virtualNetworks', 'dev-vnet')]",
-		},
-	}
-}
diff --git a/pkg/deploy/generator/resources_cluster.go b/pkg/deploy/generator/resources_cluster.go
index ede4b8cf3..c9ef7ee62 100644
--- a/pkg/deploy/generator/resources_cluster.go
+++ b/pkg/deploy/generator/resources_cluster.go
@@ -11,26 +11,11 @@ import (
 	"github.com/Azure/ARO-RP/pkg/util/azureclient"
 )
 
-func clusterVnet() *arm.Resource {
-	return &arm.Resource{
-		Resource: &mgmtnetwork.VirtualNetwork{
-			VirtualNetworkPropertiesFormat: &mgmtnetwork.VirtualNetworkPropertiesFormat{
-				AddressSpace: &mgmtnetwork.AddressSpace{
-					AddressPrefixes: &[]string{
-						"[parameters('vnetAddressPrefix')]",
-					},
-				},
-			},
-			Name:     to.StringPtr("dev-vnet"),
-			Type:     to.StringPtr("Microsoft.Network/virtualNetworks"),
-			Location: to.StringPtr("[resourceGroup().location]"),
-		},
-		Condition:  "[parameters('ci')]",
-		APIVersion: azureclient.APIVersion("Microsoft.Network"),
-	}
+func (g *generator) clusterVnet() *arm.Resource {
+	return g.virtualNetwork("dev-vnet", "[parameters('vnetAddressPrefix')]", nil, "[parameters('ci')]")
 }
 
-func clusterRouteTable() *arm.Resource {
+func (g *generator) clusterRouteTable() *arm.Resource {
 	return &arm.Resource{
 		Resource: &mgmtnetwork.RouteTable{
 			Name:     to.StringPtr("[concat(parameters('clusterName'), '-rt')]"),
@@ -41,7 +26,7 @@ func clusterRouteTable() *arm.Resource {
 	}
 }
 
-func clusterMasterSubnet() *arm.Resource {
+func (g *generator) clusterMasterSubnet() *arm.Resource {
 	return &arm.Resource{
 		Resource: &mgmtnetwork.Subnet{
 			SubnetPropertiesFormat: &mgmtnetwork.SubnetPropertiesFormat{
@@ -68,7 +53,7 @@ func clusterMasterSubnet() *arm.Resource {
 	}
 }
 
-func clusterWorkerSubnet() *arm.Resource {
+func (g *generator) clusterWorkerSubnet() *arm.Resource {
 	return &arm.Resource{
 		Resource: &mgmtnetwork.Subnet{
 			SubnetPropertiesFormat: &mgmtnetwork.SubnetPropertiesFormat{
diff --git a/pkg/deploy/generator/resources_dev.go b/pkg/deploy/generator/resources_dev.go
new file mode 100644
index 000000000..c8d3a4536
--- /dev/null
+++ b/pkg/deploy/generator/resources_dev.go
@@ -0,0 +1,469 @@
+package generator
+
+// Copyright (c) Microsoft Corporation.
+// Licensed under the Apache License 2.0.
+
+import (
+	"encoding/base64"
+	"fmt"
+	"strings"
+
+	mgmtcompute "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2020-06-01/compute"
+	mgmtnetwork "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2019-07-01/network"
+	"github.com/Azure/go-autorest/autorest/to"
+
+	"github.com/Azure/ARO-RP/pkg/util/arm"
+	"github.com/Azure/ARO-RP/pkg/util/azureclient"
+)
+
+func (g *generator) devProxyVMSS() *arm.Resource {
+	parts := []string{
+		fmt.Sprintf("base64ToString('%s')", base64.StdEncoding.EncodeToString([]byte("set -ex\n\n"))),
+	}
+
+	for _, variable := range []string{"proxyImage", "proxyImageAuth"} {
+		parts = append(parts,
+			fmt.Sprintf("'%s=$(base64 -d <<<'''", strings.ToUpper(variable)),
+			fmt.Sprintf("base64(parameters('%s'))", variable),
+			"''')\n'",
+		)
+	}
+
+	for _, variable := range []string{"proxyCert", "proxyClientCert", "proxyKey"} {
+		parts = append(parts,
+			fmt.Sprintf("'%s='''", strings.ToUpper(variable)),
+			fmt.Sprintf("parameters('%s')", variable),
+			"'''\n'",
+		)
+	}
+
+	trailer := base64.StdEncoding.EncodeToString([]byte(`yum -y update -x WALinuxAgent
+yum -y install docker
+
+firewall-cmd --add-port=443/tcp --permanent
+
+mkdir /root/.docker
+cat >/root/.docker/config.json <<EOF
+{
+	"auths": {
+		"${PROXYIMAGE%%/*}": {
+			"auth": "$PROXYIMAGEAUTH"
+		}
+	}
+}
+EOF
+systemctl start docker.service
+docker pull "$PROXYIMAGE"
+
+mkdir /etc/proxy
+base64 -d <<<"$PROXYCERT" >/etc/proxy/proxy.crt
+base64 -d <<<"$PROXYKEY" >/etc/proxy/proxy.key
+base64 -d <<<"$PROXYCLIENTCERT" >/etc/proxy/proxy-client.crt
+chown -R 1000:1000 /etc/proxy
+chmod 0600 /etc/proxy/proxy.key
+
+cat >/etc/sysconfig/proxy <<EOF
+PROXY_IMAGE='$PROXYIMAGE'
+EOF
+
+cat >/etc/systemd/system/proxy.service <<'EOF'
+[Unit]
+After=docker.service
+Requires=docker.service
+
+[Service]
+EnvironmentFile=/etc/sysconfig/proxy
+ExecStartPre=-/usr/bin/docker rm -f %n
+ExecStart=/usr/bin/docker run --rm --name %n -p 443:8443 -v /etc/proxy:/secrets $PROXY_IMAGE
+ExecStop=/usr/bin/docker stop %n
+Restart=always
+RestartSec=1
+StartLimitInterval=0
+
+[Install]
+WantedBy=multi-user.target
+EOF
+
+systemctl enable proxy.service
+
+(sleep 30; reboot) &
+`))
+
+	parts = append(parts, "'\n'", fmt.Sprintf("base64ToString('%s')", trailer))
+
+	script := fmt.Sprintf("[base64(concat(%s))]", strings.Join(parts, ","))
+
+	return &arm.Resource{
+		Resource: &mgmtcompute.VirtualMachineScaleSet{
+			Sku: &mgmtcompute.Sku{
+				Name:     to.StringPtr(string(mgmtcompute.VirtualMachineSizeTypesStandardD2sV3)),
+				Tier:     to.StringPtr("Standard"),
+				Capacity: to.Int64Ptr(1),
+			},
+			VirtualMachineScaleSetProperties: &mgmtcompute.VirtualMachineScaleSetProperties{
+				UpgradePolicy: &mgmtcompute.UpgradePolicy{
+					Mode: mgmtcompute.UpgradeModeManual,
+				},
+				VirtualMachineProfile: &mgmtcompute.VirtualMachineScaleSetVMProfile{
+					OsProfile: &mgmtcompute.VirtualMachineScaleSetOSProfile{
+						ComputerNamePrefix: to.StringPtr("dev-proxy-"),
+						AdminUsername:      to.StringPtr("cloud-user"),
+						LinuxConfiguration: &mgmtcompute.LinuxConfiguration{
+							DisablePasswordAuthentication: to.BoolPtr(true),
+							SSH: &mgmtcompute.SSHConfiguration{
+								PublicKeys: &[]mgmtcompute.SSHPublicKey{
+									{
+										Path:    to.StringPtr("/home/cloud-user/.ssh/authorized_keys"),
+										KeyData: to.StringPtr("[parameters('sshPublicKey')]"),
+									},
+								},
+							},
+						},
+					},
+					StorageProfile: &mgmtcompute.VirtualMachineScaleSetStorageProfile{
+						ImageReference: &mgmtcompute.ImageReference{
+							Publisher: to.StringPtr("RedHat"),
+							Offer:     to.StringPtr("RHEL"),
+							Sku:       to.StringPtr("7-LVM"),
+							Version:   to.StringPtr("latest"),
+						},
+						OsDisk: &mgmtcompute.VirtualMachineScaleSetOSDisk{
+							CreateOption: mgmtcompute.DiskCreateOptionTypesFromImage,
+							ManagedDisk: &mgmtcompute.VirtualMachineScaleSetManagedDiskParameters{
+								StorageAccountType: mgmtcompute.StorageAccountTypesPremiumLRS,
+							},
+						},
+					},
+					NetworkProfile: &mgmtcompute.VirtualMachineScaleSetNetworkProfile{
+						NetworkInterfaceConfigurations: &[]mgmtcompute.VirtualMachineScaleSetNetworkConfiguration{
+							{
+								Name: to.StringPtr("dev-proxy-vmss-nic"),
+								VirtualMachineScaleSetNetworkConfigurationProperties: &mgmtcompute.VirtualMachineScaleSetNetworkConfigurationProperties{
+									Primary: to.BoolPtr(true),
+									IPConfigurations: &[]mgmtcompute.VirtualMachineScaleSetIPConfiguration{
+										{
+											Name: to.StringPtr("dev-proxy-vmss-ipconfig"),
+											VirtualMachineScaleSetIPConfigurationProperties: &mgmtcompute.VirtualMachineScaleSetIPConfigurationProperties{
+												Subnet: &mgmtcompute.APIEntityReference{
+													ID: to.StringPtr("[resourceId('Microsoft.Network/virtualNetworks/subnets', 'rp-vnet', 'rp-subnet')]"),
+												},
+												Primary: to.BoolPtr(true),
+												PublicIPAddressConfiguration: &mgmtcompute.VirtualMachineScaleSetPublicIPAddressConfiguration{
+													Name: to.StringPtr("dev-proxy-vmss-pip"),
+													VirtualMachineScaleSetPublicIPAddressConfigurationProperties: &mgmtcompute.VirtualMachineScaleSetPublicIPAddressConfigurationProperties{
+														DNSSettings: &mgmtcompute.VirtualMachineScaleSetPublicIPAddressConfigurationDNSSettings{
+															DomainNameLabel: to.StringPtr("[parameters('proxyDomainNameLabel')]"),
+														},
+													},
+												},
+											},
+										},
+									},
+								},
+							},
+						},
+					},
+					ExtensionProfile: &mgmtcompute.VirtualMachineScaleSetExtensionProfile{
+						Extensions: &[]mgmtcompute.VirtualMachineScaleSetExtension{
+							{
+								Name: to.StringPtr("dev-proxy-vmss-cse"),
+								VirtualMachineScaleSetExtensionProperties: &mgmtcompute.VirtualMachineScaleSetExtensionProperties{
+									Publisher:               to.StringPtr("Microsoft.Azure.Extensions"),
+									Type:                    to.StringPtr("CustomScript"),
+									TypeHandlerVersion:      to.StringPtr("2.0"),
+									AutoUpgradeMinorVersion: to.BoolPtr(true),
+									Settings:                map[string]interface{}{},
+									ProtectedSettings: map[string]interface{}{
+										"script": script,
+									},
+								},
+							},
+						},
+					},
+				},
+				Overprovision: to.BoolPtr(false),
+			},
+			Name:     to.StringPtr("dev-proxy-vmss"),
+			Type:     to.StringPtr("Microsoft.Compute/virtualMachineScaleSets"),
+			Location: to.StringPtr("[resourceGroup().location]"),
+		},
+		APIVersion: azureclient.APIVersion("Microsoft.Compute"),
+	}
+}
+
+func (g *generator) devVPNPip() *arm.Resource {
+	return &arm.Resource{
+		Resource: &mgmtnetwork.PublicIPAddress{
+			Sku: &mgmtnetwork.PublicIPAddressSku{
+				Name: "[parameters('publicIPAddressSkuName')]",
+			},
+			PublicIPAddressPropertiesFormat: &mgmtnetwork.PublicIPAddressPropertiesFormat{
+				PublicIPAllocationMethod: "[parameters('publicIPAddressAllocationMethod')]",
+			},
+			Name:     to.StringPtr("dev-vpn-pip"),
+			Type:     to.StringPtr("Microsoft.Network/publicIPAddresses"),
+			Location: to.StringPtr("[resourceGroup().location]"),
+		},
+		Condition:  "[equals(parameters('ciCapacity'), 0)]", // TODO(mj): Refactor g.conditionStanza for better usage
+		APIVersion: azureclient.APIVersion("Microsoft.Network"),
+	}
+}
+
+func (g *generator) devVnet() *arm.Resource {
+	return g.virtualNetwork("dev-vnet", "10.0.0.0/23", &[]mgmtnetwork.Subnet{
+		{
+			SubnetPropertiesFormat: &mgmtnetwork.SubnetPropertiesFormat{
+				AddressPrefix: to.StringPtr("10.0.0.0/24"),
+			},
+			Name: to.StringPtr("GatewaySubnet"),
+		},
+		{
+			SubnetPropertiesFormat: &mgmtnetwork.SubnetPropertiesFormat{
+				AddressPrefix: to.StringPtr("10.0.1.0/24"),
+				NetworkSecurityGroup: &mgmtnetwork.SecurityGroup{
+					ID: to.StringPtr("[resourceId('Microsoft.Network/networkSecurityGroups', 'rp-nsg')]"),
+				},
+			},
+			Name: to.StringPtr("ToolingSubnet"),
+		},
+	}, nil)
+}
+
+func (g *generator) devVPN() *arm.Resource {
+	return &arm.Resource{
+		Resource: &mgmtnetwork.VirtualNetworkGateway{
+			VirtualNetworkGatewayPropertiesFormat: &mgmtnetwork.VirtualNetworkGatewayPropertiesFormat{
+				IPConfigurations: &[]mgmtnetwork.VirtualNetworkGatewayIPConfiguration{
+					{
+						VirtualNetworkGatewayIPConfigurationPropertiesFormat: &mgmtnetwork.VirtualNetworkGatewayIPConfigurationPropertiesFormat{
+							Subnet: &mgmtnetwork.SubResource{
+								ID: to.StringPtr("[resourceId('Microsoft.Network/virtualNetworks/subnets', 'dev-vnet', 'GatewaySubnet')]"),
+							},
+							PublicIPAddress: &mgmtnetwork.SubResource{
+								ID: to.StringPtr("[resourceId('Microsoft.Network/publicIPAddresses', 'dev-vpn-pip')]"),
+							},
+						},
+						Name: to.StringPtr("default"),
+					},
+				},
+				VpnType: mgmtnetwork.RouteBased,
+				Sku: &mgmtnetwork.VirtualNetworkGatewaySku{
+					Name: mgmtnetwork.VirtualNetworkGatewaySkuNameVpnGw1,
+					Tier: mgmtnetwork.VirtualNetworkGatewaySkuTierVpnGw1,
+				},
+				VpnClientConfiguration: &mgmtnetwork.VpnClientConfiguration{
+					VpnClientAddressPool: &mgmtnetwork.AddressSpace{
+						AddressPrefixes: &[]string{"192.168.255.0/24"},
+					},
+					VpnClientRootCertificates: &[]mgmtnetwork.VpnClientRootCertificate{
+						{
+							VpnClientRootCertificatePropertiesFormat: &mgmtnetwork.VpnClientRootCertificatePropertiesFormat{
+								PublicCertData: to.StringPtr("[parameters('vpnCACertificate')]"),
+							},
+							Name: to.StringPtr("dev-vpn-ca"),
+						},
+					},
+					VpnClientProtocols: &[]mgmtnetwork.VpnClientProtocol{
+						mgmtnetwork.OpenVPN,
+					},
+				},
+			},
+			Name:     to.StringPtr("dev-vpn"),
+			Type:     to.StringPtr("Microsoft.Network/virtualNetworkGateways"),
+			Location: to.StringPtr("[resourceGroup().location]"),
+		},
+		Condition:  "[equals(parameters('ciCapacity'), 0)]", // TODO(mj): Refactor g.conditionStanza for better usage
+		APIVersion: azureclient.APIVersion("Microsoft.Network"),
+		DependsOn: []string{
+			"[resourceId('Microsoft.Network/publicIPAddresses', 'dev-vpn-pip')]",
+			"[resourceId('Microsoft.Network/virtualNetworks', 'dev-vnet')]",
+		},
+	}
+}
+
+func (g *generator) devCIPool() *arm.Resource {
+	parts := []string{
+		fmt.Sprintf("base64ToString('%s')", base64.StdEncoding.EncodeToString([]byte("set -e\n\n"))),
+	}
+
+	for _, variable := range []string{
+		"ciAzpToken",
+		"ciPoolName"} {
+		parts = append(parts,
+			fmt.Sprintf("'%s='''", strings.ToUpper(variable)),
+			fmt.Sprintf("parameters('%s')", variable),
+			"'''\n'",
+		)
+	}
+
+	trailer := base64.StdEncoding.EncodeToString([]byte(`
+yum -y update -x WALinuxAgent
+
+lvextend -l +50%FREE /dev/rootvg/varlv
+xfs_growfs /var
+
+lvextend -l +100%FREE /dev/rootvg/homelv
+xfs_growfs /home
+
+rpm --import https://dl.fedoraproject.org/pub/epel/RPM-GPG-KEY-EPEL-7
+rpm --import https://packages.microsoft.com/keys/microsoft.asc
+
+yum -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
+
+cat >/etc/yum.repos.d/azure.repo <<'EOF'
+[azure-cli]
+name=azure-cli
+baseurl=https://packages.microsoft.com/yumrepos/azure-cli
+enabled=yes
+gpgcheck=yes
+EOF
+
+yum --enablerepo=rhui-rhel-7-server-rhui-optional-rpms -y install azure-cli docker jq libassuan-devel gcc gpgme-devel rh-git29 rh-python36 tmpwatch lttng-usr
+
+GO_VERSION=1.14.9
+curl https://dl.google.com/go/go${GO_VERSION}.linux-amd64.tar.gz | tar -C /usr/local -xz
+ln -s /usr/local/go/bin/* /usr/local/bin
+
+VSTS_AGENT_VERSION=2.181.1
+mkdir /home/cloud-user/agent
+pushd /home/cloud-user/agent
+curl https://vstsagentpackage.azureedge.net/agent/${VSTS_AGENT_VERSION}/vsts-agent-linux-x64-${VSTS_AGENT_VERSION}.tar.gz | tar -xz
+chown -R cloud-user:cloud-user .
+
+./bin/installdependencies.sh
+sudo -u cloud-user ./config.sh --unattended --url https://dev.azure.com/msazure --auth pat --token "$CIAZPTOKEN" --pool "$CIPOOLNAME" --agent "ARO-RHEL-$HOSTNAME" --replace
+./svc.sh install cloud-user
+popd
+
+# merge in /opt/rh/rh-*/enable
+cat >/home/cloud-user/agent/.path <<'EOF'
+/opt/rh/rh-python36/root/usr/bin:/opt/rh/rh-git29/root/usr/bin:/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/cloud-user/.local/bin:/home/cloud-user/bin
+EOF
+
+cat >/home/cloud-user/agent/.env <<'EOF'
+LD_LIBRARY_PATH=/opt/rh/rh-python36/root/usr/lib64:/opt/rh/httpd24/root/usr/lib64
+MANPATH=/opt/rh/rh-python36/root/usr/share/man:/opt/rh/rh-git29/root/usr/share/man
+PERL5LIB=/opt/rh/rh-git29/root/usr/share/perl5/vendor_perl
+PKG_CONFIG_PATH=/opt/rh/rh-python36/root/usr/lib64/pkgconfig
+XDG_DATA_DIRS=/opt/rh/rh-python36/root/usr/share:/usr/local/share:/usr/share
+EOF
+
+sed -i -e 's/^OPTIONS='\''/OPTIONS='\''-G cloud-user /' /etc/sysconfig/docker
+
+systemctl enable docker
+
+cat >/etc/cron.hourly/tmpwatch <<'EOF'
+#!/bin/bash
+
+exec /sbin/tmpwatch 24h /tmp
+EOF
+chmod +x /etc/cron.hourly/tmpwatch
+
+(sleep 30; reboot) &
+`))
+
+	parts = append(parts, "'\n'", fmt.Sprintf("base64ToString('%s')", trailer))
+
+	script := fmt.Sprintf("[base64(concat(%s))]", strings.Join(parts, ","))
+
+	return &arm.Resource{
+		Resource: &mgmtcompute.VirtualMachineScaleSet{
+			Sku: &mgmtcompute.Sku{
+				Name:     to.StringPtr(string(mgmtcompute.VirtualMachineSizeTypesStandardD2sV3)),
+				Tier:     to.StringPtr("Standard"),
+				Capacity: to.Int64Ptr(1337),
+			},
+			VirtualMachineScaleSetProperties: &mgmtcompute.VirtualMachineScaleSetProperties{
+				UpgradePolicy: &mgmtcompute.UpgradePolicy{
+					Mode: mgmtcompute.UpgradeModeManual,
+				},
+				VirtualMachineProfile: &mgmtcompute.VirtualMachineScaleSetVMProfile{
+					OsProfile: &mgmtcompute.VirtualMachineScaleSetOSProfile{
+						ComputerNamePrefix: to.StringPtr("ci-"),
+						AdminUsername:      to.StringPtr("cloud-user"),
+						LinuxConfiguration: &mgmtcompute.LinuxConfiguration{
+							DisablePasswordAuthentication: to.BoolPtr(true),
+							SSH: &mgmtcompute.SSHConfiguration{
+								PublicKeys: &[]mgmtcompute.SSHPublicKey{
+									{
+										Path:    to.StringPtr("/home/cloud-user/.ssh/authorized_keys"),
+										KeyData: to.StringPtr("[parameters('sshPublicKey')]"),
+									},
+								},
+							},
+						},
+					},
+					StorageProfile: &mgmtcompute.VirtualMachineScaleSetStorageProfile{
+						ImageReference: &mgmtcompute.ImageReference{
+							Publisher: to.StringPtr("RedHat"),
+							Offer:     to.StringPtr("RHEL"),
+							Sku:       to.StringPtr("7-LVM"),
+							Version:   to.StringPtr("latest"),
+						},
+						OsDisk: &mgmtcompute.VirtualMachineScaleSetOSDisk{
+							CreateOption: mgmtcompute.DiskCreateOptionTypesFromImage,
+							ManagedDisk: &mgmtcompute.VirtualMachineScaleSetManagedDiskParameters{
+								StorageAccountType: mgmtcompute.StorageAccountTypesPremiumLRS,
+							},
+						},
+					},
+					NetworkProfile: &mgmtcompute.VirtualMachineScaleSetNetworkProfile{
+						NetworkInterfaceConfigurations: &[]mgmtcompute.VirtualMachineScaleSetNetworkConfiguration{
+							{
+								Name: to.StringPtr("ci-vmss-nic"),
+								VirtualMachineScaleSetNetworkConfigurationProperties: &mgmtcompute.VirtualMachineScaleSetNetworkConfigurationProperties{
+									Primary: to.BoolPtr(true),
+									IPConfigurations: &[]mgmtcompute.VirtualMachineScaleSetIPConfiguration{
+										{
+											Name: to.StringPtr("ci-vmss-ipconfig"),
+											VirtualMachineScaleSetIPConfigurationProperties: &mgmtcompute.VirtualMachineScaleSetIPConfigurationProperties{
+												Subnet: &mgmtcompute.APIEntityReference{
+													ID: to.StringPtr("[resourceId('Microsoft.Network/virtualNetworks/subnets', 'dev-vnet', 'ToolingSubnet')]"),
+												},
+												Primary: to.BoolPtr(true),
+												PublicIPAddressConfiguration: &mgmtcompute.VirtualMachineScaleSetPublicIPAddressConfiguration{
+													Name: to.StringPtr("ci-vmss-pip"),
+													VirtualMachineScaleSetPublicIPAddressConfigurationProperties: &mgmtcompute.VirtualMachineScaleSetPublicIPAddressConfigurationProperties{
+														DNSSettings: &mgmtcompute.VirtualMachineScaleSetPublicIPAddressConfigurationDNSSettings{
+															DomainNameLabel: to.StringPtr("aro-ci"),
+														},
+													},
+												},
+											},
+										},
+									},
+								},
+							},
+						},
+					},
+					ExtensionProfile: &mgmtcompute.VirtualMachineScaleSetExtensionProfile{
+						Extensions: &[]mgmtcompute.VirtualMachineScaleSetExtension{
+							{
+								Name: to.StringPtr("ci-vmss-cse"),
+								VirtualMachineScaleSetExtensionProperties: &mgmtcompute.VirtualMachineScaleSetExtensionProperties{
+									Publisher:               to.StringPtr("Microsoft.Azure.Extensions"),
+									Type:                    to.StringPtr("CustomScript"),
+									TypeHandlerVersion:      to.StringPtr("2.0"),
+									AutoUpgradeMinorVersion: to.BoolPtr(true),
+									Settings:                map[string]interface{}{},
+									ProtectedSettings: map[string]interface{}{
+										"script": script,
+									},
+								},
+							},
+						},
+					},
+				},
+				Overprovision: to.BoolPtr(false),
+			},
+			Name:     to.StringPtr("ci-vmss"),
+			Type:     to.StringPtr("Microsoft.Compute/virtualMachineScaleSets"),
+			Location: to.StringPtr("[resourceGroup().location]"),
+		},
+		APIVersion: azureclient.APIVersion("Microsoft.Compute"),
+		Condition:  "[greater(parameters('ciCapacity'), 0)]", // TODO(mj): Refactor g.conditionStanza for better usage
+		DependsOn: []string{
+			"[resourceId('Microsoft.Network/virtualNetworks', 'dev-vnet')]",
+		},
+	}
+}
diff --git a/pkg/deploy/generator/resources_rp.go b/pkg/deploy/generator/resources_rp.go
new file mode 100644
index 000000000..70b65254f
--- /dev/null
+++ b/pkg/deploy/generator/resources_rp.go
@@ -0,0 +1,1491 @@
+package generator
+
+// Copyright (c) Microsoft Corporation.
+// Licensed under the Apache License 2.0.
+
+import (
+	"encoding/base64"
+	"fmt"
+	"strings"
+
+	mgmtcompute "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2020-06-01/compute"
+	mgmtdocumentdb "github.com/Azure/azure-sdk-for-go/services/cosmos-db/mgmt/2019-08-01/documentdb"
+	mgmtdns "github.com/Azure/azure-sdk-for-go/services/dns/mgmt/2018-05-01/dns"
+	mgmtkeyvault "github.com/Azure/azure-sdk-for-go/services/keyvault/mgmt/2016-10-01/keyvault"
+	mgmtmsi "github.com/Azure/azure-sdk-for-go/services/msi/mgmt/2018-11-30/msi"
+	mgmtnetwork "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2019-07-01/network"
+	mgmtauthorization "github.com/Azure/azure-sdk-for-go/services/preview/authorization/mgmt/2018-09-01-preview/authorization"
+	mgmtcontainerregistry "github.com/Azure/azure-sdk-for-go/services/preview/containerregistry/mgmt/2019-06-01-preview/containerregistry"
+	mgmtinsights "github.com/Azure/azure-sdk-for-go/services/preview/monitor/mgmt/2018-03-01/insights"
+	mgmtstorage "github.com/Azure/azure-sdk-for-go/services/storage/mgmt/2019-04-01/storage"
+	"github.com/Azure/go-autorest/autorest/to"
+
+	"github.com/Azure/ARO-RP/pkg/env"
+	"github.com/Azure/ARO-RP/pkg/util/arm"
+	"github.com/Azure/ARO-RP/pkg/util/azureclient"
+	"github.com/Azure/ARO-RP/pkg/util/rbac"
+	"github.com/Azure/ARO-RP/pkg/util/version"
+)
+
+func (g *generator) rpManagedIdentity() *arm.Resource {
+	return &arm.Resource{
+		Resource: &mgmtmsi.Identity{
+			Type:     to.StringPtr("Microsoft.ManagedIdentity/userAssignedIdentities"),
+			Name:     to.StringPtr("[concat('aro-rp-', resourceGroup().location)]"),
+			Location: to.StringPtr("[resourceGroup().location]"),
+		},
+		APIVersion: azureclient.APIVersion("Microsoft.ManagedIdentity"),
+	}
+}
+
+func (g *generator) rpSecurityGroup() *arm.Resource {
+	rules := []mgmtnetwork.SecurityRule{
+		{
+			SecurityRulePropertiesFormat: &mgmtnetwork.SecurityRulePropertiesFormat{
+				Protocol:                 mgmtnetwork.SecurityRuleProtocolTCP,
+				SourcePortRange:          to.StringPtr("*"),
+				DestinationPortRange:     to.StringPtr("443"),
+				SourceAddressPrefix:      to.StringPtr("AzureResourceManager"),
+				DestinationAddressPrefix: to.StringPtr("*"),
+				Access:                   mgmtnetwork.SecurityRuleAccessAllow,
+				Priority:                 to.Int32Ptr(120),
+				Direction:                mgmtnetwork.SecurityRuleDirectionInbound,
+			},
+			Name: to.StringPtr("rp_in_arm"),
+		},
+	}
+
+	if !g.production {
+		// override production ARM flag for more open configuration in development
+		rules[0].SecurityRulePropertiesFormat.SourceAddressPrefix = to.StringPtr("*")
+
+		rules = append(rules, mgmtnetwork.SecurityRule{
+			SecurityRulePropertiesFormat: &mgmtnetwork.SecurityRulePropertiesFormat{
+				Protocol:                 mgmtnetwork.SecurityRuleProtocolTCP,
+				SourcePortRange:          to.StringPtr("*"),
+				DestinationPortRange:     to.StringPtr("22"),
+				SourceAddressPrefix:      to.StringPtr("*"),
+				DestinationAddressPrefix: to.StringPtr("*"),
+				Access:                   mgmtnetwork.SecurityRuleAccessAllow,
+				Priority:                 to.Int32Ptr(125),
+				Direction:                mgmtnetwork.SecurityRuleDirectionInbound,
+			},
+			Name: to.StringPtr("ssh_in"),
+		})
+	} else {
+		rules = append(rules, mgmtnetwork.SecurityRule{
+			SecurityRulePropertiesFormat: &mgmtnetwork.SecurityRulePropertiesFormat{
+				Protocol:                 mgmtnetwork.SecurityRuleProtocolTCP,
+				SourcePortRange:          to.StringPtr("*"),
+				DestinationPortRange:     to.StringPtr("443"),
+				SourceAddressPrefixes:    to.StringSlicePtr([]string{}),
+				Access:                   mgmtnetwork.SecurityRuleAccessAllow,
+				DestinationAddressPrefix: to.StringPtr("*"),
+				Priority:                 to.Int32Ptr(130),
+				Direction:                mgmtnetwork.SecurityRuleDirectionInbound,
+			},
+			Name: to.StringPtr("rp_in_geneva"),
+		})
+	}
+
+	return g.securityGroup("rp-nsg", &rules, g.conditionStanza("deployNSGs"))
+}
+
+func (g *generator) rpPESecurityGroup() *arm.Resource {
+	return g.securityGroup("rp-pe-nsg", nil, g.conditionStanza("deployNSGs"))
+}
+
+func (g *generator) rpVnet() *arm.Resource {
+	subnet := mgmtnetwork.Subnet{
+		SubnetPropertiesFormat: &mgmtnetwork.SubnetPropertiesFormat{
+			AddressPrefix: to.StringPtr("10.0.0.0/24"),
+			NetworkSecurityGroup: &mgmtnetwork.SecurityGroup{
+				ID: to.StringPtr("[resourceId('Microsoft.Network/networkSecurityGroups', 'rp-nsg')]"),
+			},
+		},
+		Name: to.StringPtr("rp-subnet"),
+	}
+
+	if g.production {
+		subnet.ServiceEndpoints = &[]mgmtnetwork.ServiceEndpointPropertiesFormat{
+			{
+				Service:   to.StringPtr("Microsoft.KeyVault"),
+				Locations: &[]string{"*"},
+			},
+			{
+				Service:   to.StringPtr("Microsoft.AzureCosmosDB"),
+				Locations: &[]string{"*"},
+			},
+		}
+	}
+
+	return g.virtualNetwork("rp-vnet", "10.0.0.0/24", &[]mgmtnetwork.Subnet{subnet}, nil)
+}
+
+func (g *generator) rpPEVnet() *arm.Resource {
+	return g.virtualNetwork("rp-pe-vnet-001", "10.0.4.0/22", &[]mgmtnetwork.Subnet{
+		{
+			SubnetPropertiesFormat: &mgmtnetwork.SubnetPropertiesFormat{
+				AddressPrefix: to.StringPtr("10.0.4.0/22"),
+				NetworkSecurityGroup: &mgmtnetwork.SecurityGroup{
+					ID: to.StringPtr("[resourceId('Microsoft.Network/networkSecurityGroups', 'rp-pe-nsg')]"),
+				},
+				PrivateEndpointNetworkPolicies: to.StringPtr("Disabled"),
+			},
+			Name: to.StringPtr("rp-pe-subnet"),
+		},
+	}, nil)
+}
+
+func (g *generator) rpLB() *arm.Resource {
+	return &arm.Resource{
+		Resource: &mgmtnetwork.LoadBalancer{
+			Sku: &mgmtnetwork.LoadBalancerSku{
+				Name: mgmtnetwork.LoadBalancerSkuNameStandard,
+			},
+			LoadBalancerPropertiesFormat: &mgmtnetwork.LoadBalancerPropertiesFormat{
+				FrontendIPConfigurations: &[]mgmtnetwork.FrontendIPConfiguration{
+					{
+						FrontendIPConfigurationPropertiesFormat: &mgmtnetwork.FrontendIPConfigurationPropertiesFormat{
+							PublicIPAddress: &mgmtnetwork.PublicIPAddress{
+								ID: to.StringPtr("[resourceId('Microsoft.Network/publicIPAddresses', 'rp-pip')]"),
+							},
+						},
+						Name: to.StringPtr("rp-frontend"),
+					},
+					{
+						FrontendIPConfigurationPropertiesFormat: &mgmtnetwork.FrontendIPConfigurationPropertiesFormat{
+							PublicIPAddress: &mgmtnetwork.PublicIPAddress{
+								ID: to.StringPtr("[resourceId('Microsoft.Network/publicIPAddresses', 'portal-pip')]"),
+							},
+						},
+						Name: to.StringPtr("portal-frontend"),
+					},
+				},
+				BackendAddressPools: &[]mgmtnetwork.BackendAddressPool{
+					{
+						Name: to.StringPtr("rp-backend"),
+					},
+				},
+				LoadBalancingRules: &[]mgmtnetwork.LoadBalancingRule{
+					{
+						LoadBalancingRulePropertiesFormat: &mgmtnetwork.LoadBalancingRulePropertiesFormat{
+							FrontendIPConfiguration: &mgmtnetwork.SubResource{
+								ID: to.StringPtr("[resourceId('Microsoft.Network/loadBalancers/frontendIPConfigurations', 'rp-lb', 'rp-frontend')]"),
+							},
+							BackendAddressPool: &mgmtnetwork.SubResource{
+								ID: to.StringPtr("[resourceId('Microsoft.Network/loadBalancers/backendAddressPools', 'rp-lb', 'rp-backend')]"),
+							},
+							Probe: &mgmtnetwork.SubResource{
+								ID: to.StringPtr("[resourceId('Microsoft.Network/loadBalancers/probes', 'rp-lb', 'rp-probe')]"),
+							},
+							Protocol:         mgmtnetwork.TransportProtocolTCP,
+							LoadDistribution: mgmtnetwork.LoadDistributionDefault,
+							FrontendPort:     to.Int32Ptr(443),
+							BackendPort:      to.Int32Ptr(443),
+						},
+						Name: to.StringPtr("rp-lbrule"),
+					},
+					{
+						LoadBalancingRulePropertiesFormat: &mgmtnetwork.LoadBalancingRulePropertiesFormat{
+							FrontendIPConfiguration: &mgmtnetwork.SubResource{
+								ID: to.StringPtr("[resourceId('Microsoft.Network/loadBalancers/frontendIPConfigurations', 'rp-lb', 'portal-frontend')]"),
+							},
+							BackendAddressPool: &mgmtnetwork.SubResource{
+								ID: to.StringPtr("[resourceId('Microsoft.Network/loadBalancers/backendAddressPools', 'rp-lb', 'rp-backend')]"),
+							},
+							Probe: &mgmtnetwork.SubResource{
+								ID: to.StringPtr("[resourceId('Microsoft.Network/loadBalancers/probes', 'rp-lb', 'portal-probe-https')]"),
+							},
+							Protocol:         mgmtnetwork.TransportProtocolTCP,
+							LoadDistribution: mgmtnetwork.LoadDistributionDefault,
+							FrontendPort:     to.Int32Ptr(443),
+							BackendPort:      to.Int32Ptr(444),
+						},
+						Name: to.StringPtr("portal-lbrule"),
+					},
+					{
+						LoadBalancingRulePropertiesFormat: &mgmtnetwork.LoadBalancingRulePropertiesFormat{
+							FrontendIPConfiguration: &mgmtnetwork.SubResource{
+								ID: to.StringPtr("[resourceId('Microsoft.Network/loadBalancers/frontendIPConfigurations', 'rp-lb', 'portal-frontend')]"),
+							},
+							BackendAddressPool: &mgmtnetwork.SubResource{
+								ID: to.StringPtr("[resourceId('Microsoft.Network/loadBalancers/backendAddressPools', 'rp-lb', 'rp-backend')]"),
+							},
+							Probe: &mgmtnetwork.SubResource{
+								ID: to.StringPtr("[resourceId('Microsoft.Network/loadBalancers/probes', 'rp-lb', 'portal-probe-ssh')]"),
+							},
+							Protocol:         mgmtnetwork.TransportProtocolTCP,
+							LoadDistribution: mgmtnetwork.LoadDistributionDefault,
+							FrontendPort:     to.Int32Ptr(22),
+							BackendPort:      to.Int32Ptr(2222),
+						},
+						Name: to.StringPtr("portal-lbrule-ssh"),
+					},
+				},
+				Probes: &[]mgmtnetwork.Probe{
+					{
+						ProbePropertiesFormat: &mgmtnetwork.ProbePropertiesFormat{
+							Protocol:       mgmtnetwork.ProbeProtocolHTTPS,
+							Port:           to.Int32Ptr(443),
+							NumberOfProbes: to.Int32Ptr(2),
+							RequestPath:    to.StringPtr("/healthz/ready"),
+						},
+						Name: to.StringPtr("rp-probe"),
+					},
+					{
+						ProbePropertiesFormat: &mgmtnetwork.ProbePropertiesFormat{
+							Protocol:       mgmtnetwork.ProbeProtocolHTTPS,
+							Port:           to.Int32Ptr(444),
+							NumberOfProbes: to.Int32Ptr(2),
+							RequestPath:    to.StringPtr("/healthz/ready"),
+						},
+						Name: to.StringPtr("portal-probe-https"),
+					},
+					{
+						ProbePropertiesFormat: &mgmtnetwork.ProbePropertiesFormat{
+							Protocol:       mgmtnetwork.ProbeProtocolTCP,
+							Port:           to.Int32Ptr(2222),
+							NumberOfProbes: to.Int32Ptr(2),
+						},
+						Name: to.StringPtr("portal-probe-ssh"),
+					},
+				},
+			},
+			Name:     to.StringPtr("rp-lb"),
+			Type:     to.StringPtr("Microsoft.Network/loadBalancers"),
+			Location: to.StringPtr("[resourceGroup().location]"),
+		},
+		APIVersion: azureclient.APIVersion("Microsoft.Network"),
+		DependsOn: []string{
+			"[resourceId('Microsoft.Network/publicIPAddresses', 'portal-pip')]",
+			"[resourceId('Microsoft.Network/publicIPAddresses', 'rp-pip')]",
+		},
+	}
+}
+
+// rpLBAlert generates an alert resource for the rp-lb healthprobe metric
+func (g *generator) rpLBAlert(threshold float64, severity int32, name string, evalFreq string, windowSize string, metric string) *arm.Resource {
+	return &arm.Resource{
+		Resource: mgmtinsights.MetricAlertResource{
+			MetricAlertProperties: &mgmtinsights.MetricAlertProperties{
+				Actions: &[]mgmtinsights.MetricAlertAction{
+					{
+						ActionGroupID: to.StringPtr("[resourceId(parameters('subscriptionResourceGroupName'), 'Microsoft.Insights/actionGroups', 'rp-health-ag')]"),
+					},
+				},
+				Enabled:             to.BoolPtr(true),
+				EvaluationFrequency: to.StringPtr(evalFreq),
+				Severity:            to.Int32Ptr(severity),
+				Scopes: &[]string{
+					"[resourceId('Microsoft.Network/loadBalancers', 'rp-lb')]",
+				},
+				WindowSize:         to.StringPtr(windowSize),
+				TargetResourceType: to.StringPtr("Microsoft.Network/loadBalancers"),
+				AutoMitigate:       to.BoolPtr(true),
+				Criteria: mgmtinsights.MetricAlertSingleResourceMultipleMetricCriteria{
+					AllOf: &[]mgmtinsights.MetricCriteria{
+						{
+							CriterionType:   mgmtinsights.CriterionTypeStaticThresholdCriterion,
+							MetricName:      to.StringPtr(metric),
+							MetricNamespace: to.StringPtr("microsoft.network/loadBalancers"),
+							Name:            to.StringPtr("HealthProbeCheck"),
+							Operator:        mgmtinsights.OperatorLessThan,
+							Threshold:       to.Float64Ptr(threshold),
+							TimeAggregation: mgmtinsights.Average,
+						},
+					},
+					OdataType: mgmtinsights.OdataTypeMicrosoftAzureMonitorSingleResourceMultipleMetricCriteria,
+				},
+			},
+			Name:     to.StringPtr("[concat('" + name + "-', resourceGroup().location)]"),
+			Type:     to.StringPtr("Microsoft.Insights/metricAlerts"),
+			Location: to.StringPtr("global"),
+		},
+		APIVersion: azureclient.APIVersion("Microsoft.Insights"),
+		DependsOn: []string{
+			"[resourceId('Microsoft.Network/loadBalancers', 'rp-lb')]",
+		},
+	}
+}
+
+func (g *generator) rpVMSS() *arm.Resource {
+	parts := []string{
+		fmt.Sprintf("base64ToString('%s')", base64.StdEncoding.EncodeToString([]byte("set -ex\n\n"))),
+	}
+
+	for _, variable := range []string{
+		"mdmFrontendUrl",
+		"mdsdConfigVersion",
+		"mdsdEnvironment",
+		"acrResourceId",
+		"clusterParentDomainName",
+		"portalAccessGroupIds",
+		"portalClientId",
+		"portalElevatedGroupIds",
+		"rpImage",
+		"rpMode",
+		"rpParentDomainName",
+		"adminApiClientCertCommonName",
+		"databaseAccountName",
+		"keyvaultPrefix",
+	} {
+		parts = append(parts,
+			fmt.Sprintf("'%s=$(base64 -d <<<'''", strings.ToUpper(variable)),
+			fmt.Sprintf("base64(parameters('%s'))", variable),
+			"''')\n'",
+		)
+	}
+
+	for _, variable := range []string{
+		"adminApiCaBundle",
+	} {
+		parts = append(parts,
+			fmt.Sprintf("'%s='''", strings.ToUpper(variable)),
+			fmt.Sprintf("parameters('%s')", variable),
+			"'''\n'",
+		)
+	}
+
+	parts = append(parts,
+		"'MDMIMAGE=''"+version.MdmImage("")+"''\n'",
+	)
+
+	parts = append(parts,
+		"'LOCATION=$(base64 -d <<<'''",
+		"base64(resourceGroup().location)",
+		"''')\n'",
+	)
+
+	parts = append(parts,
+		"'SUBSCRIPTIONID=$(base64 -d <<<'''",
+		"base64(subscription().subscriptionId)",
+		"''')\n'",
+	)
+
+	parts = append(parts,
+		"'RESOURCEGROUPNAME=$(base64 -d <<<'''",
+		"base64(resourceGroup().name)",
+		"''')\n'",
+	)
+
+	trailer := base64.StdEncoding.EncodeToString([]byte(`
+yum -y update -x WALinuxAgent
+
+lvextend -l +50%FREE /dev/rootvg/rootlv
+xfs_growfs /
+
+lvextend -l +100%FREE /dev/rootvg/varlv
+xfs_growfs /var
+
+# avoid "error: db5 error(-30969) from dbenv->open: BDB0091 DB_VERSION_MISMATCH: Database environment version mismatch"
+rm -f /var/lib/rpm/__db*
+
+rpm --import https://dl.fedoraproject.org/pub/epel/RPM-GPG-KEY-EPEL-7
+rpm --import https://packages.microsoft.com/keys/microsoft.asc
+rpm --import https://packages.fluentbit.io/fluentbit.key
+
+for attempt in {1..5}; do
+  yum -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm && break
+  if [[ ${attempt} -lt 5 ]]; then sleep 10; else exit 1; fi
+done
+
+cat >/etc/yum.repos.d/azure.repo <<'EOF'
+[azure-cli]
+name=azure-cli
+baseurl=https://packages.microsoft.com/yumrepos/azure-cli
+enabled=yes
+gpgcheck=yes
+
+[azurecore]
+name=azurecore
+baseurl=https://packages.microsoft.com/yumrepos/azurecore
+enabled=yes
+gpgcheck=no
+EOF
+
+cat >/etc/yum.repos.d/td-agent-bit.repo <<'EOF'
+[td-agent-bit]
+name=td-agent-bit
+baseurl=https://packages.fluentbit.io/centos/7/$basearch
+enabled=yes
+gpgcheck=yes
+EOF
+
+for attempt in {1..5}; do
+yum --enablerepo=rhui-rhel-7-server-rhui-optional-rpms -y install azsec-clamav azsec-monitor azure-cli azure-mdsd azure-security docker openssl-perl td-agent-bit && break
+  if [[ ${attempt} -lt 5 ]]; then sleep 10; else exit 1; fi
+done
+
+rpm -e $(rpm -qa | grep ^abrt-)
+cat >/etc/sysctl.d/01-disable-core.conf <<'EOF'
+kernel.core_pattern = |/bin/true
+EOF
+sysctl --system
+
+firewall-cmd --add-port=443/tcp --permanent
+firewall-cmd --add-port=444/tcp --permanent
+firewall-cmd --add-port=2222/tcp --permanent
+
+cat >/etc/td-agent-bit/td-agent-bit.conf <<'EOF'
+[INPUT]
+	Name systemd
+	Tag journald
+	Systemd_Filter _COMM=aro
+
+[FILTER]
+	Name modify
+	Match journald
+	Remove_wildcard _
+	Remove TIMESTAMP
+
+[FILTER]
+	Name rewrite_tag
+	Match journald
+	Rule $LOGKIND ifxaudit ifxaudit false
+
+[OUTPUT]
+	Name forward
+	Match *
+	Port 29230
+EOF
+
+az login -i
+az account set -s "$SUBSCRIPTIONID"
+
+systemctl start docker.service
+az acr login --name "$(sed -e 's|.*/||' <<<"$ACRRESOURCEID")"
+
+MDMIMAGE="${RPIMAGE%%/*}/${MDMIMAGE##*/}"
+docker pull "$MDMIMAGE"
+docker pull "$RPIMAGE"
+
+az logout
+
+mkdir /etc/aro-rp
+base64 -d <<<"$ADMINAPICABUNDLE" >/etc/aro-rp/admin-ca-bundle.pem
+chown -R 1000:1000 /etc/aro-rp
+
+cat >/etc/sysconfig/mdm <<EOF
+MDMFRONTENDURL='$MDMFRONTENDURL'
+MDMIMAGE='$MDMIMAGE'
+MDMSOURCEENVIRONMENT='$LOCATION'
+MDMSOURCEROLE=rp
+MDMSOURCEROLEINSTANCE='$(hostname)'
+EOF
+
+mkdir /var/etw
+cat >/etc/systemd/system/mdm.service <<'EOF'
+[Unit]
+After=docker.service
+Requires=docker.service
+
+[Service]
+EnvironmentFile=/etc/sysconfig/mdm
+ExecStartPre=-/usr/bin/docker rm -f %N
+ExecStart=/usr/bin/docker run \
+  --entrypoint /usr/sbin/MetricsExtension \
+  --hostname %H \
+  --name %N \
+  --rm \
+  -m 2g \
+  -v /etc/mdm.pem:/etc/mdm.pem \
+  -v /var/etw:/var/etw:z \
+  $MDMIMAGE \
+  -CertFile /etc/mdm.pem \
+  -FrontEndUrl $MDMFRONTENDURL \
+  -Logger Console \
+  -LogLevel Warning \
+  -PrivateKeyFile /etc/mdm.pem \
+  -SourceEnvironment $MDMSOURCEENVIRONMENT \
+  -SourceRole $MDMSOURCEROLE \
+  -SourceRoleInstance $MDMSOURCEROLEINSTANCE
+ExecStop=/usr/bin/docker stop %N
+Restart=always
+RestartSec=1
+StartLimitInterval=0
+
+[Install]
+WantedBy=multi-user.target
+EOF
+
+cat >/etc/sysconfig/aro-rp <<EOF
+ACR_RESOURCE_ID='$ACRRESOURCEID'
+ADMIN_API_CLIENT_CERT_COMMON_NAME='$ADMINAPICLIENTCERTCOMMONNAME'
+DATABASE_ACCOUNT_NAME='$DATABASEACCOUNTNAME'
+DOMAIN_NAME='$LOCATION.$CLUSTERPARENTDOMAINNAME'
+KEYVAULT_PREFIX='$KEYVAULTPREFIX'
+MDM_ACCOUNT=AzureRedHatOpenShiftRP
+MDM_NAMESPACE=RP
+RP_MODE='$RPMODE'
+RPIMAGE='$RPIMAGE'
+EOF
+
+cat >/etc/systemd/system/aro-rp.service <<'EOF'
+[Unit]
+After=docker.service
+Requires=docker.service
+
+[Service]
+EnvironmentFile=/etc/sysconfig/aro-rp
+ExecStartPre=-/usr/bin/docker rm -f %N
+ExecStart=/usr/bin/docker run \
+  --hostname %H \
+  --name %N \
+  --rm \
+  -e ACR_RESOURCE_ID \
+  -e ADMIN_API_CLIENT_CERT_COMMON_NAME \
+  -e DATABASE_ACCOUNT_NAME \
+  -e DOMAIN_NAME \
+  -e KEYVAULT_PREFIX \
+  -e MDM_ACCOUNT \
+  -e MDM_NAMESPACE \
+  -e RP_MODE \
+  -m 2g \
+  -p 443:8443 \
+  -v /etc/aro-rp:/etc/aro-rp \
+  -v /run/systemd/journal:/run/systemd/journal \
+  -v /var/etw:/var/etw:z \
+  $RPIMAGE \
+  rp
+ExecStop=/usr/bin/docker stop -t 3600 %N
+TimeoutStopSec=3600
+Restart=always
+RestartSec=1
+StartLimitInterval=0
+
+[Install]
+WantedBy=multi-user.target
+EOF
+
+cat >/etc/sysconfig/aro-monitor <<EOF
+CLUSTER_MDM_ACCOUNT=AzureRedHatOpenShiftCluster
+CLUSTER_MDM_NAMESPACE=BBM
+DATABASE_ACCOUNT_NAME='$DATABASEACCOUNTNAME'
+KEYVAULT_PREFIX='$KEYVAULTPREFIX'
+MDM_ACCOUNT=AzureRedHatOpenShiftRP
+MDM_NAMESPACE=BBM
+RP_MODE='$RPMODE'
+RPIMAGE='$RPIMAGE'
+EOF
+
+cat >/etc/systemd/system/aro-monitor.service <<'EOF'
+[Unit]
+After=docker.service
+Requires=docker.service
+
+[Service]
+EnvironmentFile=/etc/sysconfig/aro-monitor
+ExecStartPre=-/usr/bin/docker rm -f %N
+ExecStart=/usr/bin/docker run \
+  --hostname %H \
+  --name %N \
+  --rm \
+  -e CLUSTER_MDM_ACCOUNT \
+  -e CLUSTER_MDM_NAMESPACE \
+  -e DATABASE_ACCOUNT_NAME \
+  -e KEYVAULT_PREFIX \
+  -e MDM_ACCOUNT \
+  -e MDM_NAMESPACE \
+  -e RP_MODE \
+  -m 2g \
+  -v /run/systemd/journal:/run/systemd/journal \
+  -v /var/etw:/var/etw:z \
+  $RPIMAGE \
+  monitor
+Restart=always
+RestartSec=1
+StartLimitInterval=0
+
+[Install]
+WantedBy=multi-user.target
+EOF
+
+cat >/etc/sysconfig/aro-portal <<EOF
+AZURE_PORTAL_ACCESS_GROUP_IDS='$PORTALACCESSGROUPIDS'
+AZURE_PORTAL_CLIENT_ID='$PORTALCLIENTID'
+AZURE_PORTAL_ELEVATED_GROUP_IDS='$PORTALELEVATEDGROUPIDS'
+DATABASE_ACCOUNT_NAME='$DATABASEACCOUNTNAME'
+KEYVAULT_PREFIX='$KEYVAULTPREFIX'
+MDM_ACCOUNT=AzureRedHatOpenShiftRP
+MDM_NAMESPACE=Portal
+PORTAL_HOSTNAME='$LOCATION.admin.$RPPARENTDOMAINNAME'
+RP_MODE='$RPMODE'
+RPIMAGE='$RPIMAGE'
+EOF
+
+cat >/etc/systemd/system/aro-portal.service <<'EOF'
+[Unit]
+After=docker.service
+Requires=docker.service
+StartLimitInterval=0
+
+[Service]
+EnvironmentFile=/etc/sysconfig/aro-portal
+ExecStartPre=-/usr/bin/docker rm -f %N
+ExecStart=/usr/bin/docker run \
+  --hostname %H \
+  --name %N \
+  --rm \
+  -e ADMIN_API_CLIENT_CERT_COMMON_NAME \
+  -e AZURE_PORTAL_ACCESS_GROUP_IDS \
+  -e AZURE_PORTAL_CLIENT_ID \
+  -e AZURE_PORTAL_ELEVATED_GROUP_IDS \
+  -e DATABASE_ACCOUNT_NAME \
+  -e KEYVAULT_PREFIX \
+  -e MDM_ACCOUNT \
+  -e MDM_NAMESPACE \
+  -e PORTAL_HOSTNAME \
+  -e RP_MODE \
+  -m 2g \
+  -p 444:8444 \
+  -p 2222:2222 \
+  -v /run/systemd/journal:/run/systemd/journal \
+  -v /var/etw:/var/etw:z \
+  $RPIMAGE \
+  portal
+Restart=always
+RestartSec=1
+
+[Install]
+WantedBy=multi-user.target
+EOF
+
+chcon -R system_u:object_r:var_log_t:s0 /var/opt/microsoft/linuxmonagent
+
+mkdir -p /var/lib/waagent/Microsoft.Azure.KeyVault.Store
+
+for var in "mdsd" "mdm"; do
+cat >/etc/systemd/system/download-$var-credentials.service <<EOF
+[Unit]
+Description=Periodic $var credentials refresh
+
+[Service]
+Type=oneshot
+ExecStart=/usr/local/bin/download-credentials.sh $var
+EOF
+
+cat >/etc/systemd/system/download-$var-credentials.timer <<EOF
+[Unit]
+Description=Periodic $var credentials refresh
+
+[Timer]
+OnBootSec=0min
+OnCalendar=0/12:00:00
+AccuracySec=5s
+
+[Install]
+WantedBy=timers.target
+EOF
+done
+
+cat >/usr/local/bin/download-credentials.sh <<EOF
+#!/bin/bash
+set -eu
+
+COMPONENT="\$1"
+echo "Download \$COMPONENT credentials"
+
+TEMP_DIR=\$(mktemp -d)
+export AZURE_CONFIG_DIR=\$(mktemp -d)
+az login -i
+az account set -s "$SUBSCRIPTIONID"
+
+trap "cleanup" EXIT
+
+cleanup() {
+  az logout
+  [[ "\$TEMP_DIR" =~ /tmp/.+ ]] && rm -rf \$TEMP_DIR
+  [[ "\$AZURE_CONFIG_DIR" =~ /tmp/.+ ]] && rm -rf \$AZURE_CONFIG_DIR
+}
+
+if [ "\$COMPONENT" = "mdm" ]; then
+  CURRENT_CERT_FILE="/etc/mdm.pem"
+elif [ "\$COMPONENT" = "mdsd" ]; then
+  CURRENT_CERT_FILE="/var/lib/waagent/Microsoft.Azure.KeyVault.Store/mdsd.pem"
+else
+  echo Invalid usage && exit 1
+fi
+
+SECRET_NAME="rp-\${COMPONENT}"
+NEW_CERT_FILE="\$TEMP_DIR/\$COMPONENT.pem"
+for attempt in {1..5}; do
+  az keyvault secret download --file \$NEW_CERT_FILE --id "https://$KEYVAULTPREFIX-svc.vault.azure.net/secrets/\$SECRET_NAME" && break
+  if [[ \$attempt -lt 5 ]]; then sleep 10; else exit 1; fi
+done
+
+if [ -f \$NEW_CERT_FILE ]; then
+  if [ "\$COMPONENT" = "mdsd" ]; then
+    chown syslog:syslog \$NEW_CERT_FILE
+  else
+    sed -i -ne '1,/END CERTIFICATE/ p' \$NEW_CERT_FILE
+  fi
+  chmod 0600 \$NEW_CERT_FILE
+  mv \$NEW_CERT_FILE \$CURRENT_CERT_FILE
+else
+  echo Failed to refresh certificate for \$COMPONENT && exit 1
+fi
+EOF
+
+chmod u+x /usr/local/bin/download-credentials.sh
+
+systemctl enable download-mdsd-credentials.timer
+systemctl enable download-mdm-credentials.timer
+
+/usr/local/bin/download-credentials.sh mdsd
+/usr/local/bin/download-credentials.sh mdm
+MDSDCERTIFICATESAN=$(openssl x509 -in /var/lib/waagent/Microsoft.Azure.KeyVault.Store/mdsd.pem -noout -subject | sed -e 's/.*CN=//')
+
+mkdir /etc/systemd/system/mdsd.service.d
+cat >/etc/systemd/system/mdsd.service.d/override.conf <<'EOF'
+[Unit]
+After=network-online.target
+EOF
+
+cat >/etc/default/mdsd <<EOF
+MDSD_ROLE_PREFIX=/var/run/mdsd/default
+MDSD_OPTIONS="-A -d -r \$MDSD_ROLE_PREFIX"
+
+export MONITORING_GCS_ENVIRONMENT='$MDSDENVIRONMENT'
+export MONITORING_GCS_ACCOUNT=ARORPLogs
+export MONITORING_GCS_REGION='$LOCATION'
+export MONITORING_GCS_AUTH_ID_TYPE=AuthKeyVault
+export MONITORING_GCS_AUTH_ID='$MDSDCERTIFICATESAN'
+export MONITORING_GCS_NAMESPACE=ARORPLogs
+export MONITORING_CONFIG_VERSION='$MDSDCONFIGVERSION'
+export MONITORING_USE_GENEVA_CONFIG_SERVICE=true
+
+export MONITORING_TENANT='$LOCATION'
+export MONITORING_ROLE=rp
+export MONITORING_ROLE_INSTANCE='$(hostname)'
+EOF
+
+# setting MONITORING_GCS_AUTH_ID_TYPE=AuthKeyVault seems to have caused mdsd not
+# to honour SSL_CERT_FILE any more, heaven only knows why.
+mkdir -p /usr/lib/ssl/certs
+csplit -f /usr/lib/ssl/certs/cert- -b %03d.pem /etc/pki/tls/certs/ca-bundle.crt /^$/1 {*} >/dev/null
+c_rehash /usr/lib/ssl/certs
+
+for service in aro-monitor aro-portal aro-rp auoms azsecd azsecmond mdsd mdm chronyd td-agent-bit; do
+  systemctl enable $service.service
+done
+
+for scan in baseline clamav software; do
+  /usr/local/bin/azsecd config -s $scan -d P1D
+done
+
+(sleep 30; reboot) &
+`))
+
+	parts = append(parts, "'\n'", fmt.Sprintf("base64ToString('%s')", trailer))
+
+	script := fmt.Sprintf("[base64(concat(%s))]", strings.Join(parts, ","))
+
+	return &arm.Resource{
+		Resource: &mgmtcompute.VirtualMachineScaleSet{
+			Sku: &mgmtcompute.Sku{
+				Name:     to.StringPtr("[parameters('vmSize')]"),
+				Tier:     to.StringPtr("Standard"),
+				Capacity: to.Int64Ptr(3),
+			},
+			VirtualMachineScaleSetProperties: &mgmtcompute.VirtualMachineScaleSetProperties{
+				UpgradePolicy: &mgmtcompute.UpgradePolicy{
+					Mode: mgmtcompute.UpgradeModeManual,
+				},
+				VirtualMachineProfile: &mgmtcompute.VirtualMachineScaleSetVMProfile{
+					OsProfile: &mgmtcompute.VirtualMachineScaleSetOSProfile{
+						ComputerNamePrefix: to.StringPtr("[concat('rp-', parameters('vmssName'), '-')]"),
+						AdminUsername:      to.StringPtr("cloud-user"),
+						LinuxConfiguration: &mgmtcompute.LinuxConfiguration{
+							DisablePasswordAuthentication: to.BoolPtr(true),
+							SSH: &mgmtcompute.SSHConfiguration{
+								PublicKeys: &[]mgmtcompute.SSHPublicKey{
+									{
+										Path:    to.StringPtr("/home/cloud-user/.ssh/authorized_keys"),
+										KeyData: to.StringPtr("[parameters('sshPublicKey')]"),
+									},
+								},
+							},
+						},
+					},
+					StorageProfile: &mgmtcompute.VirtualMachineScaleSetStorageProfile{
+						ImageReference: &mgmtcompute.ImageReference{
+							Publisher: to.StringPtr("RedHat"),
+							Offer:     to.StringPtr("RHEL"),
+							Sku:       to.StringPtr("7-LVM"),
+							Version:   to.StringPtr("latest"),
+						},
+						OsDisk: &mgmtcompute.VirtualMachineScaleSetOSDisk{
+							CreateOption: mgmtcompute.DiskCreateOptionTypesFromImage,
+							ManagedDisk: &mgmtcompute.VirtualMachineScaleSetManagedDiskParameters{
+								StorageAccountType: mgmtcompute.StorageAccountTypesPremiumLRS,
+							},
+						},
+					},
+					NetworkProfile: &mgmtcompute.VirtualMachineScaleSetNetworkProfile{
+						HealthProbe: &mgmtcompute.APIEntityReference{
+							ID: to.StringPtr("[resourceId('Microsoft.Network/loadBalancers/probes', 'rp-lb', 'rp-probe')]"),
+						},
+						NetworkInterfaceConfigurations: &[]mgmtcompute.VirtualMachineScaleSetNetworkConfiguration{
+							{
+								Name: to.StringPtr("rp-vmss-nic"),
+								VirtualMachineScaleSetNetworkConfigurationProperties: &mgmtcompute.VirtualMachineScaleSetNetworkConfigurationProperties{
+									Primary: to.BoolPtr(true),
+									IPConfigurations: &[]mgmtcompute.VirtualMachineScaleSetIPConfiguration{
+										{
+											Name: to.StringPtr("rp-vmss-ipconfig"),
+											VirtualMachineScaleSetIPConfigurationProperties: &mgmtcompute.VirtualMachineScaleSetIPConfigurationProperties{
+												Subnet: &mgmtcompute.APIEntityReference{
+													ID: to.StringPtr("[resourceId('Microsoft.Network/virtualNetworks/subnets', 'rp-vnet', 'rp-subnet')]"),
+												},
+												Primary: to.BoolPtr(true),
+												PublicIPAddressConfiguration: &mgmtcompute.VirtualMachineScaleSetPublicIPAddressConfiguration{
+													Name: to.StringPtr("rp-vmss-pip"),
+												},
+												LoadBalancerBackendAddressPools: &[]mgmtcompute.SubResource{
+													{
+														ID: to.StringPtr("[resourceId('Microsoft.Network/loadBalancers/backendAddressPools', 'rp-lb', 'rp-backend')]"),
+													},
+												},
+											},
+										},
+									},
+								},
+							},
+						},
+					},
+					SecurityProfile: &mgmtcompute.SecurityProfile{
+						EncryptionAtHost: to.BoolPtr(true),
+					},
+					ExtensionProfile: &mgmtcompute.VirtualMachineScaleSetExtensionProfile{
+						Extensions: &[]mgmtcompute.VirtualMachineScaleSetExtension{
+							{
+								Name: to.StringPtr("rp-vmss-cse"),
+								VirtualMachineScaleSetExtensionProperties: &mgmtcompute.VirtualMachineScaleSetExtensionProperties{
+									Publisher:               to.StringPtr("Microsoft.Azure.Extensions"),
+									Type:                    to.StringPtr("CustomScript"),
+									TypeHandlerVersion:      to.StringPtr("2.0"),
+									AutoUpgradeMinorVersion: to.BoolPtr(true),
+									Settings:                map[string]interface{}{},
+									ProtectedSettings: map[string]interface{}{
+										"script": script,
+									},
+								},
+							},
+						},
+					},
+					DiagnosticsProfile: &mgmtcompute.DiagnosticsProfile{
+						BootDiagnostics: &mgmtcompute.BootDiagnostics{
+							Enabled:    to.BoolPtr(true),
+							StorageURI: to.StringPtr("[concat('https://', parameters('storageAccountDomain'), '/')]"),
+						},
+					},
+				},
+				Overprovision: to.BoolPtr(false),
+			},
+			Identity: &mgmtcompute.VirtualMachineScaleSetIdentity{
+				Type: mgmtcompute.ResourceIdentityTypeUserAssigned,
+				UserAssignedIdentities: map[string]*mgmtcompute.VirtualMachineScaleSetIdentityUserAssignedIdentitiesValue{
+					"[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', concat('aro-rp-', resourceGroup().location))]": {},
+				},
+			},
+			Name:     to.StringPtr("[concat('rp-vmss-', parameters('vmssName'))]"),
+			Type:     to.StringPtr("Microsoft.Compute/virtualMachineScaleSets"),
+			Location: to.StringPtr("[resourceGroup().location]"),
+		},
+		APIVersion: azureclient.APIVersion("Microsoft.Compute"),
+		DependsOn: []string{
+			"[resourceId('Microsoft.Authorization/roleAssignments', guid(resourceGroup().id, parameters('rpServicePrincipalId'), 'RP / Reader'))]",
+			"[resourceId('Microsoft.Network/virtualNetworks', 'rp-vnet')]",
+			"[resourceId('Microsoft.Network/loadBalancers', 'rp-lb')]",
+			"[resourceId('Microsoft.Storage/storageAccounts', substring(parameters('storageAccountDomain'), 0, indexOf(parameters('storageAccountDomain'), '.')))]",
+		},
+	}
+}
+
+func (g *generator) rpDNSZone() *arm.Resource {
+	return &arm.Resource{
+		Resource: &mgmtdns.Zone{
+			ZoneProperties: &mgmtdns.ZoneProperties{},
+			Name:           to.StringPtr("[concat(resourceGroup().location, '.', parameters('clusterParentDomainName'))]"),
+			Type:           to.StringPtr("Microsoft.Network/dnsZones"),
+			Location:       to.StringPtr("global"),
+		},
+		APIVersion: azureclient.APIVersion("Microsoft.Network/dnsZones"),
+	}
+}
+
+func (g *generator) rpClusterKeyvaultAccessPolicies() []mgmtkeyvault.AccessPolicyEntry {
+	return []mgmtkeyvault.AccessPolicyEntry{
+		// TODO: remove fpServicePrincipalId after next RP rollout
+		{
+			TenantID: &tenantUUIDHack,
+			ObjectID: to.StringPtr("[parameters('fpServicePrincipalId')]"),
+			Permissions: &mgmtkeyvault.Permissions{
+				Secrets: &[]mgmtkeyvault.SecretPermissions{
+					mgmtkeyvault.SecretPermissionsGet,
+				},
+				Certificates: &[]mgmtkeyvault.CertificatePermissions{
+					mgmtkeyvault.Create,
+					mgmtkeyvault.Delete,
+					mgmtkeyvault.Get,
+					mgmtkeyvault.Update,
+				},
+			},
+		},
+		{
+			TenantID: &tenantUUIDHack,
+			ObjectID: to.StringPtr("[parameters('rpServicePrincipalId')]"),
+			Permissions: &mgmtkeyvault.Permissions{
+				Secrets: &[]mgmtkeyvault.SecretPermissions{
+					mgmtkeyvault.SecretPermissionsGet,
+				},
+				Certificates: &[]mgmtkeyvault.CertificatePermissions{
+					mgmtkeyvault.Create,
+					mgmtkeyvault.Delete,
+					mgmtkeyvault.Get,
+					mgmtkeyvault.Update,
+				},
+			},
+		},
+	}
+}
+
+func (g *generator) rpPortalKeyvaultAccessPolicies() []mgmtkeyvault.AccessPolicyEntry {
+	return []mgmtkeyvault.AccessPolicyEntry{
+		{
+			TenantID: &tenantUUIDHack,
+			ObjectID: to.StringPtr("[parameters('rpServicePrincipalId')]"),
+			Permissions: &mgmtkeyvault.Permissions{
+				Secrets: &[]mgmtkeyvault.SecretPermissions{
+					mgmtkeyvault.SecretPermissionsGet,
+				},
+			},
+		},
+	}
+}
+
+func (g *generator) rpServiceKeyvaultAccessPolicies() []mgmtkeyvault.AccessPolicyEntry {
+	return []mgmtkeyvault.AccessPolicyEntry{
+		{
+			TenantID: &tenantUUIDHack,
+			ObjectID: to.StringPtr("[parameters('rpServicePrincipalId')]"),
+			Permissions: &mgmtkeyvault.Permissions{
+				Secrets: &[]mgmtkeyvault.SecretPermissions{
+					mgmtkeyvault.SecretPermissionsGet,
+				},
+			},
+		},
+	}
+}
+
+func (g *generator) rpClusterKeyvault() *arm.Resource {
+	vault := &mgmtkeyvault.Vault{
+		Properties: &mgmtkeyvault.VaultProperties{
+			EnableSoftDelete: to.BoolPtr(true),
+			TenantID:         &tenantUUIDHack,
+			Sku: &mgmtkeyvault.Sku{
+				Name:   mgmtkeyvault.Standard,
+				Family: to.StringPtr("A"),
+			},
+			AccessPolicies: &[]mgmtkeyvault.AccessPolicyEntry{
+				{
+					ObjectID: to.StringPtr(clusterAccessPolicyHack),
+				},
+			},
+		},
+		Name:     to.StringPtr("[concat(parameters('keyvaultPrefix'), '" + env.ClusterKeyvaultSuffix + "')]"),
+		Type:     to.StringPtr("Microsoft.KeyVault/vaults"),
+		Location: to.StringPtr("[resourceGroup().location]"),
+	}
+
+	if !g.production {
+		*vault.Properties.AccessPolicies = append(g.rpClusterKeyvaultAccessPolicies(),
+			mgmtkeyvault.AccessPolicyEntry{
+				TenantID: &tenantUUIDHack,
+				ObjectID: to.StringPtr("[parameters('adminObjectId')]"),
+				Permissions: &mgmtkeyvault.Permissions{
+					Certificates: &[]mgmtkeyvault.CertificatePermissions{
+						mgmtkeyvault.Get,
+						mgmtkeyvault.List,
+					},
+				},
+			},
+		)
+	}
+
+	return &arm.Resource{
+		Resource:   vault,
+		APIVersion: azureclient.APIVersion("Microsoft.KeyVault"),
+	}
+}
+
+func (g *generator) rpPortalKeyvault() *arm.Resource {
+	vault := &mgmtkeyvault.Vault{
+		Properties: &mgmtkeyvault.VaultProperties{
+			EnableSoftDelete: to.BoolPtr(true),
+			TenantID:         &tenantUUIDHack,
+			Sku: &mgmtkeyvault.Sku{
+				Name:   mgmtkeyvault.Standard,
+				Family: to.StringPtr("A"),
+			},
+			AccessPolicies: &[]mgmtkeyvault.AccessPolicyEntry{
+				{
+					ObjectID: to.StringPtr(portalAccessPolicyHack),
+				},
+			},
+		},
+		Name:     to.StringPtr("[concat(parameters('keyvaultPrefix'), '" + env.PortalKeyvaultSuffix + "')]"),
+		Type:     to.StringPtr("Microsoft.KeyVault/vaults"),
+		Location: to.StringPtr("[resourceGroup().location]"),
+	}
+
+	if !g.production {
+		*vault.Properties.AccessPolicies = append(g.rpPortalKeyvaultAccessPolicies(),
+			mgmtkeyvault.AccessPolicyEntry{
+				TenantID: &tenantUUIDHack,
+				ObjectID: to.StringPtr("[parameters('adminObjectId')]"),
+				Permissions: &mgmtkeyvault.Permissions{
+					Certificates: &[]mgmtkeyvault.CertificatePermissions{
+						mgmtkeyvault.Delete,
+						mgmtkeyvault.Get,
+						mgmtkeyvault.Import,
+						mgmtkeyvault.List,
+					},
+					Secrets: &[]mgmtkeyvault.SecretPermissions{
+						mgmtkeyvault.SecretPermissionsSet,
+						mgmtkeyvault.SecretPermissionsList,
+					},
+				},
+			},
+		)
+	}
+
+	return &arm.Resource{
+		Resource:   vault,
+		APIVersion: azureclient.APIVersion("Microsoft.KeyVault"),
+	}
+}
+
+func (g *generator) rpServiceKeyvault() *arm.Resource {
+	vault := &mgmtkeyvault.Vault{
+		Properties: &mgmtkeyvault.VaultProperties{
+			EnableSoftDelete: to.BoolPtr(true),
+			TenantID:         &tenantUUIDHack,
+			Sku: &mgmtkeyvault.Sku{
+				Name:   mgmtkeyvault.Standard,
+				Family: to.StringPtr("A"),
+			},
+			AccessPolicies: &[]mgmtkeyvault.AccessPolicyEntry{
+				{
+					ObjectID: to.StringPtr(serviceAccessPolicyHack),
+				},
+			},
+		},
+		Name:     to.StringPtr("[concat(parameters('keyvaultPrefix'), '" + env.ServiceKeyvaultSuffix + "')]"),
+		Type:     to.StringPtr("Microsoft.KeyVault/vaults"),
+		Location: to.StringPtr("[resourceGroup().location]"),
+	}
+
+	if !g.production {
+		*vault.Properties.AccessPolicies = append(g.rpServiceKeyvaultAccessPolicies(),
+			mgmtkeyvault.AccessPolicyEntry{
+				TenantID: &tenantUUIDHack,
+				ObjectID: to.StringPtr("[parameters('adminObjectId')]"),
+				Permissions: &mgmtkeyvault.Permissions{
+					Certificates: &[]mgmtkeyvault.CertificatePermissions{
+						mgmtkeyvault.Delete,
+						mgmtkeyvault.Get,
+						mgmtkeyvault.Import,
+						mgmtkeyvault.List,
+					},
+					Secrets: &[]mgmtkeyvault.SecretPermissions{
+						mgmtkeyvault.SecretPermissionsSet,
+						mgmtkeyvault.SecretPermissionsList,
+					},
+				},
+			},
+		)
+	}
+
+	return &arm.Resource{
+		Resource:   vault,
+		APIVersion: azureclient.APIVersion("Microsoft.KeyVault"),
+	}
+}
+
+func (g *generator) rpCosmosDB() []*arm.Resource {
+	cosmosdb := &mgmtdocumentdb.DatabaseAccountCreateUpdateParameters{
+		Kind: mgmtdocumentdb.GlobalDocumentDB,
+		DatabaseAccountCreateUpdateProperties: &mgmtdocumentdb.DatabaseAccountCreateUpdateProperties{
+			ConsistencyPolicy: &mgmtdocumentdb.ConsistencyPolicy{
+				DefaultConsistencyLevel: mgmtdocumentdb.Strong,
+			},
+			Locations: &[]mgmtdocumentdb.Location{
+				{
+					LocationName: to.StringPtr("[resourceGroup().location]"),
+				},
+			},
+			DatabaseAccountOfferType: to.StringPtr(string(mgmtdocumentdb.Standard)),
+		},
+		Name:     to.StringPtr("[parameters('databaseAccountName')]"),
+		Type:     to.StringPtr("Microsoft.DocumentDB/databaseAccounts"),
+		Location: to.StringPtr("[resourceGroup().location]"),
+		Tags: map[string]*string{
+			"defaultExperience": to.StringPtr("Core (SQL)"),
+		},
+	}
+
+	r := &arm.Resource{
+		Resource:   cosmosdb,
+		APIVersion: azureclient.APIVersion("Microsoft.DocumentDB"),
+	}
+
+	if g.production {
+		cosmosdb.IPRangeFilter = to.StringPtr("[concat('104.42.195.92,40.76.54.131,52.176.6.30,52.169.50.45,52.187.184.26', if(equals(parameters('extraCosmosDBIPs'), ''), '', ','), parameters('extraCosmosDBIPs'))]")
+		cosmosdb.IsVirtualNetworkFilterEnabled = to.BoolPtr(true)
+		cosmosdb.VirtualNetworkRules = &[]mgmtdocumentdb.VirtualNetworkRule{
+			{
+				ID: to.StringPtr("[resourceId('Microsoft.Network/virtualNetworks/subnets', 'rp-vnet', 'rp-subnet')]"),
+			},
+		}
+		cosmosdb.DisableKeyBasedMetadataWriteAccess = to.BoolPtr(true)
+
+		r.DependsOn = append(r.DependsOn, "[resourceId('Microsoft.Network/virtualNetworks', 'rp-vnet')]")
+	}
+
+	rs := []*arm.Resource{
+		r,
+	}
+
+	if g.production {
+		rs = append(rs, g.database("'ARO'", true)...)
+	}
+
+	return rs
+}
+
+func (g *generator) database(databaseName string, addDependsOn bool) []*arm.Resource {
+	portal := &arm.Resource{
+		Resource: &mgmtdocumentdb.SQLContainerCreateUpdateParameters{
+			SQLContainerCreateUpdateProperties: &mgmtdocumentdb.SQLContainerCreateUpdateProperties{
+				Resource: &mgmtdocumentdb.SQLContainerResource{
+					ID: to.StringPtr("Portal"),
+					PartitionKey: &mgmtdocumentdb.ContainerPartitionKey{
+						Paths: &[]string{
+							"/id",
+						},
+						Kind: mgmtdocumentdb.PartitionKindHash,
+					},
+					DefaultTTL: to.Int32Ptr(-1),
+				},
+				Options: map[string]*string{},
+			},
+			Name:     to.StringPtr("[concat(parameters('databaseAccountName'), '/', " + databaseName + ", '/Portal')]"),
+			Type:     to.StringPtr("Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers"),
+			Location: to.StringPtr("[resourceGroup().location]"),
+		},
+		APIVersion: azureclient.APIVersion("Microsoft.DocumentDB"),
+		DependsOn: []string{
+			"[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlDatabases', parameters('databaseAccountName'), " + databaseName + ")]",
+		},
+	}
+
+	if g.production {
+		portal.Resource.(*mgmtdocumentdb.SQLContainerCreateUpdateParameters).SQLContainerCreateUpdateProperties.Options["throughput"] = to.StringPtr("400")
+	}
+
+	rs := []*arm.Resource{
+		{
+			Resource: &mgmtdocumentdb.SQLDatabaseCreateUpdateParameters{
+				SQLDatabaseCreateUpdateProperties: &mgmtdocumentdb.SQLDatabaseCreateUpdateProperties{
+					Resource: &mgmtdocumentdb.SQLDatabaseResource{
+						ID: to.StringPtr("[" + databaseName + "]"),
+					},
+					Options: map[string]*string{
+						"throughput": to.StringPtr("500"),
+					},
+				},
+				Name:     to.StringPtr("[concat(parameters('databaseAccountName'), '/', " + databaseName + ")]"),
+				Type:     to.StringPtr("Microsoft.DocumentDB/databaseAccounts/sqlDatabases"),
+				Location: to.StringPtr("[resourceGroup().location]"),
+			},
+			APIVersion: azureclient.APIVersion("Microsoft.DocumentDB"),
+		},
+		{
+			Resource: &mgmtdocumentdb.SQLContainerCreateUpdateParameters{
+				SQLContainerCreateUpdateProperties: &mgmtdocumentdb.SQLContainerCreateUpdateProperties{
+					Resource: &mgmtdocumentdb.SQLContainerResource{
+						ID: to.StringPtr("AsyncOperations"),
+						PartitionKey: &mgmtdocumentdb.ContainerPartitionKey{
+							Paths: &[]string{
+								"/id",
+							},
+							Kind: mgmtdocumentdb.PartitionKindHash,
+						},
+						DefaultTTL: to.Int32Ptr(7 * 86400), // 7 days
+					},
+					Options: map[string]*string{},
+				},
+				Name:     to.StringPtr("[concat(parameters('databaseAccountName'), '/', " + databaseName + ", '/AsyncOperations')]"),
+				Type:     to.StringPtr("Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers"),
+				Location: to.StringPtr("[resourceGroup().location]"),
+			},
+			APIVersion: azureclient.APIVersion("Microsoft.DocumentDB"),
+			DependsOn: []string{
+				"[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlDatabases', parameters('databaseAccountName'), " + databaseName + ")]",
+			},
+		},
+		{
+			Resource: &mgmtdocumentdb.SQLContainerCreateUpdateParameters{
+				SQLContainerCreateUpdateProperties: &mgmtdocumentdb.SQLContainerCreateUpdateProperties{
+					Resource: &mgmtdocumentdb.SQLContainerResource{
+						ID: to.StringPtr("Billing"),
+						PartitionKey: &mgmtdocumentdb.ContainerPartitionKey{
+							Paths: &[]string{
+								"/id",
+							},
+							Kind: mgmtdocumentdb.PartitionKindHash,
+						},
+					},
+					Options: map[string]*string{},
+				},
+				Name:     to.StringPtr("[concat(parameters('databaseAccountName'), '/', " + databaseName + ", '/Billing')]"),
+				Type:     to.StringPtr("Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers"),
+				Location: to.StringPtr("[resourceGroup().location]"),
+			},
+			APIVersion: azureclient.APIVersion("Microsoft.DocumentDB"),
+			DependsOn: []string{
+				"[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlDatabases', parameters('databaseAccountName'), " + databaseName + ")]",
+			},
+		},
+		{
+			Resource: &mgmtdocumentdb.SQLContainerCreateUpdateParameters{
+				SQLContainerCreateUpdateProperties: &mgmtdocumentdb.SQLContainerCreateUpdateProperties{
+					Resource: &mgmtdocumentdb.SQLContainerResource{
+						ID: to.StringPtr("Monitors"),
+						PartitionKey: &mgmtdocumentdb.ContainerPartitionKey{
+							Paths: &[]string{
+								"/id",
+							},
+							Kind: mgmtdocumentdb.PartitionKindHash,
+						},
+						DefaultTTL: to.Int32Ptr(-1),
+					},
+					Options: map[string]*string{},
+				},
+				Name:     to.StringPtr("[concat(parameters('databaseAccountName'), '/', " + databaseName + ", '/Monitors')]"),
+				Type:     to.StringPtr("Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers"),
+				Location: to.StringPtr("[resourceGroup().location]"),
+			},
+			APIVersion: azureclient.APIVersion("Microsoft.DocumentDB"),
+			DependsOn: []string{
+				"[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlDatabases', parameters('databaseAccountName'), " + databaseName + ")]",
+			},
+		},
+		{
+			Resource: &mgmtdocumentdb.SQLContainerCreateUpdateParameters{
+				SQLContainerCreateUpdateProperties: &mgmtdocumentdb.SQLContainerCreateUpdateProperties{
+					Resource: &mgmtdocumentdb.SQLContainerResource{
+						ID: to.StringPtr("OpenShiftClusters"),
+						PartitionKey: &mgmtdocumentdb.ContainerPartitionKey{
+							Paths: &[]string{
+								"/partitionKey",
+							},
+							Kind: mgmtdocumentdb.PartitionKindHash,
+						},
+						UniqueKeyPolicy: &mgmtdocumentdb.UniqueKeyPolicy{
+							UniqueKeys: &[]mgmtdocumentdb.UniqueKey{
+								{
+									Paths: &[]string{
+										"/key",
+									},
+								},
+								{
+									Paths: &[]string{
+										"/clusterResourceGroupIdKey",
+									},
+								},
+								{
+									Paths: &[]string{
+										"/clientIdKey",
+									},
+								},
+							},
+						},
+					},
+					Options: map[string]*string{},
+				},
+				Name:     to.StringPtr("[concat(parameters('databaseAccountName'), '/', " + databaseName + ", '/OpenShiftClusters')]"),
+				Type:     to.StringPtr("Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers"),
+				Location: to.StringPtr("[resourceGroup().location]"),
+			},
+			APIVersion: azureclient.APIVersion("Microsoft.DocumentDB"),
+			DependsOn: []string{
+				"[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlDatabases', parameters('databaseAccountName'), " + databaseName + ")]",
+			},
+		},
+		portal,
+		{
+			Resource: &mgmtdocumentdb.SQLContainerCreateUpdateParameters{
+				SQLContainerCreateUpdateProperties: &mgmtdocumentdb.SQLContainerCreateUpdateProperties{
+					Resource: &mgmtdocumentdb.SQLContainerResource{
+						ID: to.StringPtr("Subscriptions"),
+						PartitionKey: &mgmtdocumentdb.ContainerPartitionKey{
+							Paths: &[]string{
+								"/id",
+							},
+							Kind: mgmtdocumentdb.PartitionKindHash,
+						},
+					},
+					Options: map[string]*string{},
+				},
+				Name:     to.StringPtr("[concat(parameters('databaseAccountName'), '/', " + databaseName + ", '/Subscriptions')]"),
+				Type:     to.StringPtr("Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers"),
+				Location: to.StringPtr("[resourceGroup().location]"),
+			},
+			APIVersion: azureclient.APIVersion("Microsoft.DocumentDB"),
+			DependsOn: []string{
+				"[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlDatabases', parameters('databaseAccountName'), " + databaseName + ")]",
+			},
+		},
+	}
+
+	if addDependsOn {
+		for i := range rs {
+			rs[i].DependsOn = append(rs[i].DependsOn,
+				"[resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName'))]",
+			)
+		}
+	}
+
+	return rs
+}
+
+func (g *generator) rpRoleDefinitionTokenContributor() *arm.Resource {
+	return &arm.Resource{
+		Resource: &mgmtauthorization.RoleDefinition{
+			Name: to.StringPtr("48983534-3d06-4dcb-a566-08a694eb1279"),
+			Type: to.StringPtr("Microsoft.Authorization/roleDefinitions"),
+			RoleDefinitionProperties: &mgmtauthorization.RoleDefinitionProperties{
+				RoleName:         to.StringPtr("ARO v4 ContainerRegistry Token Contributor"),
+				AssignableScopes: &[]string{"[subscription().id]"},
+				Permissions: &[]mgmtauthorization.Permission{
+					{
+						Actions: &[]string{
+							"Microsoft.ContainerRegistry/registries/generateCredentials/action",
+							"Microsoft.ContainerRegistry/registries/scopeMaps/read",
+							"Microsoft.ContainerRegistry/registries/tokens/delete",
+							"Microsoft.ContainerRegistry/registries/tokens/operationStatuses/read",
+							"Microsoft.ContainerRegistry/registries/tokens/read",
+							"Microsoft.ContainerRegistry/registries/tokens/write",
+						},
+					},
+				},
+			},
+		},
+		APIVersion: azureclient.APIVersion("Microsoft.Authorization/roleDefinitions"),
+	}
+}
+
+func (g *generator) rpRBAC() []*arm.Resource {
+	return []*arm.Resource{
+		rbac.ResourceGroupRoleAssignmentWithName(
+			rbac.RoleReader,
+			"parameters('rpServicePrincipalId')",
+			"guid(resourceGroup().id, parameters('rpServicePrincipalId'), 'RP / Reader')",
+		),
+		rbac.ResourceGroupRoleAssignmentWithName(
+			rbac.RoleNetworkContributor,
+			"parameters('fpServicePrincipalId')",
+			"guid(resourceGroup().id, 'FP / Network Contributor')",
+		),
+		rbac.ResourceRoleAssignmentWithName(
+			rbac.RoleDocumentDBAccountContributor,
+			"parameters('rpServicePrincipalId')",
+			"Microsoft.DocumentDB/databaseAccounts",
+			"parameters('databaseAccountName')",
+			"concat(parameters('databaseAccountName'), '/Microsoft.Authorization/', guid(resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName')), parameters('rpServicePrincipalId'), 'RP / DocumentDB Account Contributor'))",
+		),
+		rbac.ResourceRoleAssignmentWithName(
+			rbac.RoleDNSZoneContributor,
+			"parameters('fpServicePrincipalId')",
+			"Microsoft.Network/dnsZones",
+			"concat(resourceGroup().location, '.', parameters('clusterParentDomainName'))",
+			"concat(resourceGroup().location, '.', parameters('clusterParentDomainName'), '/Microsoft.Authorization/', guid(resourceId('Microsoft.Network/dnsZones', concat(resourceGroup().location, '.', parameters('clusterParentDomainName'))), 'FP / DNS Zone Contributor'))",
+		),
+	}
+}
+
+func (g *generator) rpBillingContributorRbac() []*arm.Resource {
+	return []*arm.Resource{
+		rbac.ResourceRoleAssignmentWithName(
+			rbac.RoleDocumentDBAccountContributor,
+			"parameters('billingServicePrincipalId')",
+			"Microsoft.DocumentDB/databaseAccounts",
+			"parameters('databaseAccountName')",
+			"concat(parameters('databaseAccountName'), '/Microsoft.Authorization/', guid(resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName')), parameters('billingServicePrincipalId') , 'Billing / DocumentDB Account Contributor'))",
+		),
+	}
+}
+
+func (g *generator) rpACRReplica() *arm.Resource {
+	return &arm.Resource{
+		Resource: &mgmtcontainerregistry.Replication{
+			Name:     to.StringPtr("[concat(substring(parameters('acrResourceId'), add(lastIndexOf(parameters('acrResourceId'), '/'), 1)), '/', parameters('location'))]"),
+			Type:     to.StringPtr("Microsoft.ContainerRegistry/registries/replications"),
+			Location: to.StringPtr("[parameters('location')]"),
+		},
+		APIVersion: azureclient.APIVersion("Microsoft.ContainerRegistry"),
+	}
+}
+
+func (g *generator) rpACRRBAC() []*arm.Resource {
+	rs := []*arm.Resource{
+		rbac.ResourceRoleAssignmentWithName(
+			rbac.RoleACRPull,
+			"parameters('rpServicePrincipalId')",
+			"Microsoft.ContainerRegistry/registries",
+			"substring(parameters('acrResourceId'), add(lastIndexOf(parameters('acrResourceId'), '/'), 1))",
+			"concat(substring(parameters('acrResourceId'), add(lastIndexOf(parameters('acrResourceId'), '/'), 1)), '/', '/Microsoft.Authorization/', guid(concat(parameters('acrResourceId'), parameters('rpServicePrincipalId'), 'RP / AcrPull')))",
+		),
+		rbac.ResourceRoleAssignmentWithName(
+			"48983534-3d06-4dcb-a566-08a694eb1279", // ARO v4 ContainerRegistry Token Contributor
+			"parameters('fpServicePrincipalId')",
+			"Microsoft.ContainerRegistry/registries",
+			"substring(parameters('acrResourceId'), add(lastIndexOf(parameters('acrResourceId'), '/'), 1))",
+			"concat(substring(parameters('acrResourceId'), add(lastIndexOf(parameters('acrResourceId'), '/'), 1)), '/', '/Microsoft.Authorization/', guid(concat(parameters('acrResourceId'), 'FP / ARO v4 ContainerRegistry Token Contributor')))",
+		),
+	}
+
+	for _, r := range rs {
+		r.DependsOn = nil
+	}
+
+	return rs
+}
+
+func (g *generator) rpVersionStorageAccount() []*arm.Resource {
+	return []*arm.Resource{
+		g.storageAccount("[parameters('rpVersionStorageAccountName')]", &mgmtstorage.AccountProperties{
+			AllowBlobPublicAccess: to.BoolPtr(true),
+		}),
+		{
+			Resource: &mgmtstorage.BlobContainer{
+				Name: to.StringPtr("[concat(parameters('rpVersionStorageAccountName'), '/default/rpversion')]"),
+				Type: to.StringPtr("Microsoft.Storage/storageAccounts/blobServices/containers"),
+				ContainerProperties: &mgmtstorage.ContainerProperties{
+					PublicAccess: mgmtstorage.PublicAccessContainer,
+				},
+			},
+			APIVersion: azureclient.APIVersion("Microsoft.Storage"),
+			DependsOn: []string{
+				"[resourceId('Microsoft.Storage/storageAccounts', parameters('rpVersionStorageAccountName'))]",
+			},
+		},
+	}
+}
+
+func (g *generator) rpStorageAccount() *arm.Resource {
+	return g.storageAccount("[substring(parameters('storageAccountDomain'), 0, indexOf(parameters('storageAccountDomain'), '.'))]", nil)
+}
diff --git a/pkg/deploy/generator/templates.go b/pkg/deploy/generator/templates.go
index b38edbed3..52752c214 100644
--- a/pkg/deploy/generator/templates.go
+++ b/pkg/deploy/generator/templates.go
@@ -6,347 +6,23 @@ package generator
 import (
 	"bytes"
 	"encoding/json"
+	"regexp"
+
+	uuid "github.com/satori/go.uuid"
 
-	"github.com/Azure/ARO-RP/pkg/env"
 	"github.com/Azure/ARO-RP/pkg/util/arm"
 )
 
-func (g *generator) clusterPredeploy() *arm.Template {
-	t := templateStanza()
-
-	params := []string{
-		"clusterName",
-		"clusterServicePrincipalId",
-		"fpServicePrincipalId",
-		"ci",
-		"vnetAddressPrefix",
-		"masterAddressPrefix",
-		"workerAddressPrefix",
-	}
-
-	for _, param := range params {
-		p := &arm.TemplateParameter{Type: "string"}
-		switch param {
-		case "ci":
-			p.Type = "bool"
-			p.DefaultValue = false
-		}
-		t.Parameters[param] = p
-	}
-
-	t.Resources = append(t.Resources,
-		clusterVnet(),
-		clusterRouteTable(),
-		clusterMasterSubnet(),
-		clusterWorkerSubnet(),
-	)
-
-	return t
-}
-
-func (g *generator) managedIdentityTemplate() *arm.Template {
-	t := templateStanza()
-
-	t.Resources = append(t.Resources,
-		g.managedIdentity(),
-	)
-
-	return t
-}
-
-func (g *generator) rpTemplate() *arm.Template {
-	t := templateStanza()
-
-	params := []string{
-		"clusterParentDomainName",
-		"databaseAccountName",
-		"fpServicePrincipalId",
-		"rpServicePrincipalId",
-	}
-	if g.production {
-		params = append(params,
-			"acrResourceId",
-			"adminApiCaBundle",
-			"adminApiClientCertCommonName",
-			"billingServicePrincipalId",
-			"encryptionAtHost",
-			"extraCosmosDBIPs",
-			"keyvaultPrefix",
-			"mdmFrontendUrl",
-			"mdsdConfigVersion",
-			"mdsdEnvironment",
-			"portalAccessGroupIds",
-			"portalClientId",
-			"portalElevatedGroupIds",
-			"rpImage",
-			"rpMode",
-			"rpParentDomainName",
-			"sshPublicKey",
-			"storageAccountDomain",
-			"subscriptionResourceGroupName",
-			"vmssName",
-			"vmSize",
-		)
-	}
-
-	for _, param := range params {
-		p := &arm.TemplateParameter{Type: "string"}
-		switch param {
-		case "encryptionAtHost":
-			p.Type = "bool"
-		case "extraCosmosDBIPs", "rpMode":
-			p.DefaultValue = ""
-		case "vmSize":
-			p.DefaultValue = "Standard_D2s_v3"
-		}
-		t.Parameters[param] = p
-	}
-
-	if g.production {
-		t.Resources = append(t.Resources,
-			g.pip("rp-pip"),
-			g.pip("portal-pip"),
-			g.lb(),
-			g.vmss(),
-			g.storageAccount(),
-			g.lbAlert(30.0, 2, "rp-availability-alert", "PT5M", "PT15M", "DipAvailability"), // triggers on all 3 RPs being down for 10min, can't be >=0.3 due to deploys going down to 32% at times.
-			g.lbAlert(67.0, 3, "rp-degraded-alert", "PT15M", "PT6H", "DipAvailability"),     // 1/3 backend down for 1h or 2/3 down for 3h in the last 6h
-			g.lbAlert(33.0, 2, "rp-vnet-alert", "PT5M", "PT5M", "VipAvailability"))          // this will trigger only if the Azure network infrastructure between the loadBalancers and VMs is down for 3.5min
-		// more on alerts https://msazure.visualstudio.com/AzureRedHatOpenShift/_wiki/wikis/ARO.wiki/53765/WIP-Alerting
-		t.Resources = append(t.Resources, g.billingContributorRbac()...)
-	}
-
-	t.Resources = append(t.Resources, g.zone(),
-		g.rpvnet(), g.pevnet(),
-		g.halfPeering("rp-vnet", "rp-pe-vnet-001"),
-		g.halfPeering("rp-pe-vnet-001", "rp-vnet"))
-	t.Resources = append(t.Resources, g.cosmosdb()...)
-	t.Resources = append(t.Resources, g.rbac()...)
-
-	return t
-}
-
-func (g *generator) rpGlobalTemplate() *arm.Template {
-	t := templateStanza()
-
-	params := []string{
-		"acrResourceId",
-		"fpServicePrincipalId",
-		"rpServicePrincipalId",
-		"rpVersionStorageAccountName",
-	}
-
-	for _, param := range params {
-		t.Parameters[param] = &arm.TemplateParameter{Type: "string"}
-	}
-	t.Resources = append(t.Resources,
-		g.acrRbac()...,
-	)
-	t.Resources = append(t.Resources,
-		g.rpVersionStorageAccount()...,
-	)
-
-	return t
-}
-
-func (g *generator) rpGlobalACRReplicationTemplate() *arm.Template {
-	t := templateStanza()
-
-	params := []string{
-		"acrResourceId",
-		"location",
-	}
-
-	for _, param := range params {
-		t.Parameters[param] = &arm.TemplateParameter{Type: "string"}
-	}
-	t.Resources = append(t.Resources,
-		g.acrReplica(),
-	)
-
-	return t
-}
-
-func (g *generator) rpGlobalSubscriptionTemplate() *arm.Template {
-	t := templateStanza()
-
-	t.Resources = append(t.Resources,
-		g.roleDefinitionTokenContributor(),
-	)
-
-	return t
-}
-
-func (g *generator) rpSubscriptionTemplate() *arm.Template {
-	t := templateStanza()
-
-	t.Resources = append(t.Resources, g.actionGroup("rp-health-ag", "rphealth"))
-
-	return t
-}
-
-func (g *generator) databaseTemplate() *arm.Template {
-	t := templateStanza()
-
-	t.Resources = append(t.Resources,
-		g.database("parameters('databaseName')", false)...)
-
-	t.Parameters = map[string]*arm.TemplateParameter{
-		"databaseAccountName": {
-			Type: "string",
-		},
-		"databaseName": {
-			Type: "string",
-		},
-	}
-
-	return t
-}
-
-func (g *generator) rpParameters() *arm.Parameters {
-	t := g.rpTemplate()
-	p := parametersStanza()
-
-	for name, tp := range t.Parameters {
-		param := &arm.ParametersParameter{Value: tp.DefaultValue}
-		if param.Value == nil {
-			param.Value = ""
-		}
-		p.Parameters[name] = param
-	}
-
-	return p
-}
-
-func (g *generator) preDeployTemplate() *arm.Template {
-	t := templateStanza()
-
-	if g.production {
-		t.Variables = map[string]interface{}{
-			"clusterKeyvaultAccessPolicies": g.clusterKeyvaultAccessPolicies(),
-			"portalKeyvaultAccessPolicies":  g.portalKeyvaultAccessPolicies(),
-			"serviceKeyvaultAccessPolicies": g.serviceKeyvaultAccessPolicies(),
-		}
-	}
-
-	params := []string{
-		"keyvaultPrefix",
-		"rpServicePrincipalId",
-		"fpServicePrincipalId",
-	}
-
-	if g.production {
-		params = append(params,
-			"deployNSGs",
-			"extraClusterKeyvaultAccessPolicies",
-			"extraPortalKeyvaultAccessPolicies",
-			"extraServiceKeyvaultAccessPolicies",
-			"rpNsgSourceAddressPrefixes",
-		)
-	} else {
-		params = append(params,
-			"adminObjectId",
-		)
-	}
-
-	for _, param := range params {
-		p := &arm.TemplateParameter{Type: "string"}
-		switch param {
-		case "deployNSGs":
-			p.Type = "bool"
-			p.DefaultValue = false
-		case "extraClusterKeyvaultAccessPolicies",
-			"extraPortalKeyvaultAccessPolicies",
-			"extraServiceKeyvaultAccessPolicies":
-			p.Type = "array"
-			p.DefaultValue = []interface{}{}
-		case "rpNsgSourceAddressPrefixes":
-			p.Type = "array"
-			p.DefaultValue = []string{}
-		case "keyvaultPrefix":
-			p.MaxLength = 24 - max(len(env.ClusterKeyvaultSuffix), len(env.ServiceKeyvaultSuffix), len(env.PortalKeyvaultSuffix))
-		}
-		t.Parameters[param] = p
-	}
-
-	t.Resources = append(t.Resources,
-		g.securityGroupRP(),
-		g.securityGroupPE(),
-		// clusterKeyvault, portalKeyvault and serviceKeyvault must be in this
-		// order due to terrible bytes.Replace in templateFixup
-		g.clusterKeyvault(),
-		g.portalKeyvault(),
-		g.serviceKeyvault(),
-	)
-
-	return t
-}
-
-func (g *generator) rpPreDeployParameters() *arm.Parameters {
-	t := g.preDeployTemplate()
-	p := parametersStanza()
-
-	for name, tp := range t.Parameters {
-		param := &arm.ParametersParameter{Value: tp.DefaultValue}
-		if param.Value == nil {
-			param.Value = ""
-		}
-		p.Parameters[name] = param
-	}
-
-	return p
-}
-
-func (g *generator) sharedDevelopmentEnvTemplate() *arm.Template {
-	t := templateStanza()
-
-	t.Resources = append(t.Resources,
-		g.devVpnPip(),
-		g.devVnet(),
-		g.devVPN(),
-		g.devCIPool(),
-		g.proxyVmss())
-
-	for _, param := range []string{
-		"ciAzpToken",
-		"ciCapacity",
-		"ciPoolName",
-		"proxyCert",
-		"proxyClientCert",
-		"proxyDomainNameLabel",
-		"proxyImage",
-		"proxyImageAuth",
-		"proxyKey",
-		"publicIPAddressAllocationMethod",
-		"publicIPAddressSkuName",
-		"sshPublicKey",
-		"vpnCACertificate",
-	} {
-		typ := "string"
-		var defaultValue interface{}
-		switch param {
-		case "ciAzpToken", "ciPoolName":
-			defaultValue = ""
-		case "ciCapacity":
-			typ = "int"
-			defaultValue = 0
-		case "proxyImageAuth", "proxyKey":
-			typ = "securestring"
-		case "publicIPAddressAllocationMethod":
-			defaultValue = "Static"
-		case "publicIPAddressSkuName":
-			defaultValue = "Standard"
-		case "vpnCACertificate":
-			defaultValue = ""
-		}
-		t.Parameters[param] = &arm.TemplateParameter{
-			Type:         typ,
-			DefaultValue: defaultValue,
-		}
-	}
+const (
+	tenantIDHack            = "13805ec3-a223-47ad-ad65-8b2baf92c0fb"
+	clusterAccessPolicyHack = "e1992efe-4835-46cf-8c08-d8b8451044b8"
+	portalAccessPolicyHack  = "e5e11dae-7c49-4118-9628-e0afa4d6a502"
+	serviceAccessPolicyHack = "533a94d0-d6c2-4fca-9af1-374aa6493468"
+)
 
-	return t
-}
+var (
+	tenantUUIDHack = uuid.Must(uuid.FromString(tenantIDHack))
+)
 
 func max(is ...int) int {
 	max := is[0]
@@ -368,9 +44,9 @@ func (g *generator) templateFixup(t *arm.Template) ([]byte, error) {
 	b = bytes.ReplaceAll(b, []byte(tenantIDHack), []byte("[subscription().tenantId]"))
 	b = bytes.ReplaceAll(b, []byte(`"capacity": 1337`), []byte(`"capacity": "[int(parameters('ciCapacity'))]"`))
 	if g.production {
-		b = bytes.Replace(b, []byte(`"accessPolicies": []`), []byte(`"accessPolicies": "[concat(variables('clusterKeyvaultAccessPolicies'), parameters('extraClusterKeyvaultAccessPolicies'))]"`), 1)
-		b = bytes.Replace(b, []byte(`"accessPolicies": []`), []byte(`"accessPolicies": "[concat(variables('portalKeyvaultAccessPolicies'), parameters('extraPortalKeyvaultAccessPolicies'))]"`), 1)
-		b = bytes.Replace(b, []byte(`"accessPolicies": []`), []byte(`"accessPolicies": "[concat(variables('serviceKeyvaultAccessPolicies'), parameters('extraServiceKeyvaultAccessPolicies'))]"`), 1)
+		b = regexp.MustCompile(`(?m)"accessPolicies": \[[^]]*`+clusterAccessPolicyHack+`[^]]*\]`).ReplaceAll(b, []byte(`"accessPolicies": "[concat(variables('clusterKeyvaultAccessPolicies'), parameters('extraClusterKeyvaultAccessPolicies'))]"`))
+		b = regexp.MustCompile(`(?m)"accessPolicies": \[[^]]*`+portalAccessPolicyHack+`[^]]*\]`).ReplaceAll(b, []byte(`"accessPolicies": "[concat(variables('portalKeyvaultAccessPolicies'), parameters('extraPortalKeyvaultAccessPolicies'))]"`))
+		b = regexp.MustCompile(`(?m)"accessPolicies": \[[^]]*`+serviceAccessPolicyHack+`[^]]*\]`).ReplaceAll(b, []byte(`"accessPolicies": "[concat(variables('serviceKeyvaultAccessPolicies'), parameters('extraServiceKeyvaultAccessPolicies'))]"`))
 		b = bytes.Replace(b, []byte(`"sourceAddressPrefixes": []`), []byte(`"sourceAddressPrefixes": "[parameters('rpNsgSourceAddressPrefixes')]"`), 1)
 		b = bytes.Replace(b, []byte(`"encryptionAtHost": true`), []byte(`"encryptionAtHost": "[parameters('encryptionAtHost')]"`), 1)
 	}
diff --git a/pkg/deploy/generator/templates_cluster.go b/pkg/deploy/generator/templates_cluster.go
new file mode 100644
index 000000000..e8c948aa2
--- /dev/null
+++ b/pkg/deploy/generator/templates_cluster.go
@@ -0,0 +1,41 @@
+package generator
+
+// Copyright (c) Microsoft Corporation.
+// Licensed under the Apache License 2.0.
+
+import (
+	"github.com/Azure/ARO-RP/pkg/util/arm"
+)
+
+func (g *generator) clusterPredeploy() *arm.Template {
+	t := templateStanza()
+
+	params := []string{
+		"clusterName",
+		"clusterServicePrincipalId",
+		"fpServicePrincipalId",
+		"ci",
+		"vnetAddressPrefix",
+		"masterAddressPrefix",
+		"workerAddressPrefix",
+	}
+
+	for _, param := range params {
+		p := &arm.TemplateParameter{Type: "string"}
+		switch param {
+		case "ci":
+			p.Type = "bool"
+			p.DefaultValue = false
+		}
+		t.Parameters[param] = p
+	}
+
+	t.Resources = append(t.Resources,
+		g.clusterVnet(),
+		g.clusterRouteTable(),
+		g.clusterMasterSubnet(),
+		g.clusterWorkerSubnet(),
+	)
+
+	return t
+}
diff --git a/pkg/deploy/generator/templates_dev.go b/pkg/deploy/generator/templates_dev.go
new file mode 100644
index 000000000..7e03e29cc
--- /dev/null
+++ b/pkg/deploy/generator/templates_dev.go
@@ -0,0 +1,77 @@
+package generator
+
+// Copyright (c) Microsoft Corporation.
+// Licensed under the Apache License 2.0.
+
+import (
+	"github.com/Azure/ARO-RP/pkg/util/arm"
+)
+
+func (g *generator) devDatabaseTemplate() *arm.Template {
+	t := templateStanza()
+
+	t.Resources = append(t.Resources,
+		g.database("parameters('databaseName')", false)...)
+
+	t.Parameters = map[string]*arm.TemplateParameter{
+		"databaseAccountName": {
+			Type: "string",
+		},
+		"databaseName": {
+			Type: "string",
+		},
+	}
+
+	return t
+}
+
+func (g *generator) devSharedTemplate() *arm.Template {
+	t := templateStanza()
+
+	t.Resources = append(t.Resources,
+		g.devVPNPip(),
+		g.devVnet(),
+		g.devVPN(),
+		g.devCIPool(),
+		g.devProxyVMSS())
+
+	for _, param := range []string{
+		"ciAzpToken",
+		"ciCapacity",
+		"ciPoolName",
+		"proxyCert",
+		"proxyClientCert",
+		"proxyDomainNameLabel",
+		"proxyImage",
+		"proxyImageAuth",
+		"proxyKey",
+		"publicIPAddressAllocationMethod",
+		"publicIPAddressSkuName",
+		"sshPublicKey",
+		"vpnCACertificate",
+	} {
+		typ := "string"
+		var defaultValue interface{}
+		switch param {
+		case "ciAzpToken", "ciPoolName":
+			defaultValue = ""
+		case "ciCapacity":
+			typ = "int"
+			defaultValue = 0
+		case "proxyImageAuth", "proxyKey":
+			typ = "securestring"
+		case "publicIPAddressAllocationMethod":
+			defaultValue = "Static"
+		case "publicIPAddressSkuName":
+			defaultValue = "Standard"
+		case "vpnCACertificate":
+			defaultValue = ""
+		}
+		t.Parameters[param] = &arm.TemplateParameter{
+			Type:         typ,
+			DefaultValue: defaultValue,
+		}
+	}
+
+	return t
+}
diff --git a/pkg/deploy/generator/templates_rp.go b/pkg/deploy/generator/templates_rp.go
new file mode 100644
index 000000000..f2bc7099f
--- /dev/null
+++ b/pkg/deploy/generator/templates_rp.go
@@ -0,0 +1,242 @@
+package generator
+
+// Copyright (c) Microsoft Corporation.
+// Licensed under the Apache License 2.0.
+
+import (
+	"github.com/Azure/ARO-RP/pkg/env"
+	"github.com/Azure/ARO-RP/pkg/util/arm"
+)
+
+func (g *generator) rpManagedIdentityTemplate() *arm.Template {
+	t := templateStanza()
+
+	t.Resources = append(t.Resources,
+		g.rpManagedIdentity(),
+	)
+
+	return t
+}
+
+func (g *generator) rpTemplate() *arm.Template {
+	t := templateStanza()
+
+	params := []string{
+		"clusterParentDomainName",
+		"databaseAccountName",
+		"fpServicePrincipalId",
+		"rpServicePrincipalId",
+	}
+	if g.production {
+		params = append(params,
+			"acrResourceId",
+			"adminApiCaBundle",
+			"adminApiClientCertCommonName",
+			"billingServicePrincipalId",
+			"encryptionAtHost",
+			"extraCosmosDBIPs",
+			"keyvaultPrefix",
+			"mdmFrontendUrl",
+			"mdsdConfigVersion",
+			"mdsdEnvironment",
+			"portalAccessGroupIds",
+			"portalClientId",
+			"portalElevatedGroupIds",
+			"rpImage",
+			"rpMode",
+			"rpParentDomainName",
+			"sshPublicKey",
+			"storageAccountDomain",
+			"subscriptionResourceGroupName",
+			"vmssName",
+			"vmSize",
+		)
+	}
+
+	for _, param := range params {
+		p := &arm.TemplateParameter{Type: "string"}
+		switch param {
+		case "encryptionAtHost":
+			p.Type = "bool"
+		case "extraCosmosDBIPs", "rpMode":
+			p.DefaultValue = ""
+		case "vmSize":
+			p.DefaultValue = "Standard_D2s_v3"
+		}
+		t.Parameters[param] = p
+	}
+
+	if g.production {
+		t.Resources = append(t.Resources,
+			g.publicIPAddress("rp-pip"),
+			g.publicIPAddress("portal-pip"),
+			g.rpLB(),
+			g.rpVMSS(),
+			g.rpStorageAccount(),
+			g.rpLBAlert(30.0, 2, "rp-availability-alert", "PT5M", "PT15M", "DipAvailability"), // triggers on all 3 RPs being down for 10min, can't be >=0.3 due to deploys going down to 32% at times.
+			g.rpLBAlert(67.0, 3, "rp-degraded-alert", "PT15M", "PT6H", "DipAvailability"),     // 1/3 backend down for 1h or 2/3 down for 3h in the last 6h
+			g.rpLBAlert(33.0, 2, "rp-vnet-alert", "PT5M", "PT5M", "VipAvailability"))          // this will trigger only if the Azure network infrastructure between the loadBalancers and VMs is down for 3.5min
+		// more on alerts https://msazure.visualstudio.com/AzureRedHatOpenShift/_wiki/wikis/ARO.wiki/53765/WIP-Alerting
+		t.Resources = append(t.Resources, g.rpBillingContributorRbac()...)
+	}
+
+	t.Resources = append(t.Resources, g.rpDNSZone(),
+		g.rpVnet(), g.rpPEVnet(),
+		g.virtualNetworkPeering("rp-vnet", "rp-pe-vnet-001"),
+		g.virtualNetworkPeering("rp-pe-vnet-001", "rp-vnet"))
+	t.Resources = append(t.Resources, g.rpCosmosDB()...)
+	t.Resources = append(t.Resources, g.rpRBAC()...)
+
+	return t
+}
+
+func (g *generator) rpGlobalTemplate() *arm.Template {
+	t := templateStanza()
+
+	params := []string{
+		"acrResourceId",
+		"fpServicePrincipalId",
+		"rpServicePrincipalId",
+		"rpVersionStorageAccountName",
+	}
+
+	for _, param := range params {
+		t.Parameters[param] = &arm.TemplateParameter{Type: "string"}
+	}
+	t.Resources = append(t.Resources,
+		g.rpACRRBAC()...,
+	)
+	t.Resources = append(t.Resources,
+		g.rpVersionStorageAccount()...,
+	)
+
+	return t
+}
+
+func (g *generator) rpGlobalACRReplicationTemplate() *arm.Template {
+	t := templateStanza()
+
+	params := []string{
+		"acrResourceId",
+		"location",
+	}
+
+	for _, param := range params {
+		t.Parameters[param] = &arm.TemplateParameter{Type: "string"}
+	}
+	t.Resources = append(t.Resources,
+		g.rpACRReplica(),
+	)
+
+	return t
+}
+
+func (g *generator) rpGlobalSubscriptionTemplate() *arm.Template {
+	t := templateStanza()
+
+	t.Resources = append(t.Resources,
+		g.rpRoleDefinitionTokenContributor(),
+	)
+
+	return t
+}
+
+func (g *generator) rpSubscriptionTemplate() *arm.Template {
+	t := templateStanza()
+
+	t.Resources = append(t.Resources, g.actionGroup("rp-health-ag", "rphealth"))
+
+	return t
+}
+
+func (g *generator) rpParameters() *arm.Parameters {
+	t := g.rpTemplate()
+	p := parametersStanza()
+
+	for name, tp := range t.Parameters {
+		param := &arm.ParametersParameter{Value: tp.DefaultValue}
+		if param.Value == nil {
+			param.Value = ""
+		}
+		p.Parameters[name] = param
+	}
+
+	return p
+}
+
+func (g *generator) rpPredeployTemplate() *arm.Template {
+	t := templateStanza()
+
+	if g.production {
+		t.Variables = map[string]interface{}{
+			"clusterKeyvaultAccessPolicies": g.rpClusterKeyvaultAccessPolicies(),
+			"portalKeyvaultAccessPolicies":  g.rpPortalKeyvaultAccessPolicies(),
+			"serviceKeyvaultAccessPolicies": g.rpServiceKeyvaultAccessPolicies(),
+		}
+	}
+
+	params := []string{
+		"keyvaultPrefix",
+		"rpServicePrincipalId",
+		"fpServicePrincipalId",
+	}
+
+	if g.production {
+		params = append(params,
+			"deployNSGs",
+			"extraClusterKeyvaultAccessPolicies",
+			"extraPortalKeyvaultAccessPolicies",
+			"extraServiceKeyvaultAccessPolicies",
+			"rpNsgSourceAddressPrefixes",
+		)
+	} else {
+		params = append(params,
+			"adminObjectId",
+		)
+	}
+
+	for _, param := range params {
+		p := &arm.TemplateParameter{Type: "string"}
+		switch param {
+		case "deployNSGs":
+			p.Type = "bool"
+			p.DefaultValue = false
+		case "extraClusterKeyvaultAccessPolicies",
+			"extraPortalKeyvaultAccessPolicies",
+			"extraServiceKeyvaultAccessPolicies":
+			p.Type = "array"
+			p.DefaultValue = []interface{}{}
+		case "rpNsgSourceAddressPrefixes":
+			p.Type = "array"
+			p.DefaultValue = []string{}
+		case "keyvaultPrefix":
+			p.MaxLength = 24 - max(len(env.ClusterKeyvaultSuffix), len(env.ServiceKeyvaultSuffix), len(env.PortalKeyvaultSuffix))
+		}
+		t.Parameters[param] = p
+	}
+
+	t.Resources = append(t.Resources,
+		g.rpSecurityGroup(),
+		g.rpPESecurityGroup(),
+		g.rpClusterKeyvault(),
+		g.rpPortalKeyvault(),
+		g.rpServiceKeyvault(),
+	)
+
+	return t
+}
+
+func (g *generator) rpPredeployParameters() *arm.Parameters {
+	t := g.rpPredeployTemplate()
+	p := parametersStanza()
+
+	for name, tp := range t.Parameters {
+		param := &arm.ParametersParameter{Value: tp.DefaultValue}
+		if param.Value == nil {
+			param.Value = ""
+		}
+		p.Parameters[name] = param
+	}
+
+	return p
+}
diff --git a/pkg/deploy/predeploy.go b/pkg/deploy/predeploy.go
index a579598b8..da7cc199d 100644
--- a/pkg/deploy/predeploy.go
+++ b/pkg/deploy/predeploy.go
@@ -34,7 +34,7 @@ func (d *deployer) PreDeploy(ctx context.Context) error {
 	}
 
 	// deploy global rbac
-	err = d.deployGlobalSubscription(ctx)
+	err = d.deployRPGlobalSubscription(ctx)
 	if err != nil {
 		return err
 	}
@@ -55,25 +55,27 @@ func (d *deployer) PreDeploy(ctx context.Context) error {
 		return err
 	}
 
-	d.log.Infof("deploying rg %s in %s", d.config.ResourceGroupName, d.config.Location)
-	_, err = d.groups.CreateOrUpdate(ctx, d.config.ResourceGroupName, mgmtfeatures.ResourceGroup{
+	d.log.Infof("deploying rg %s in %s", d.config.RPResourceGroupName, d.config.Location)
+	_, err = d.groups.CreateOrUpdate(ctx, d.config.RPResourceGroupName, mgmtfeatures.ResourceGroup{
 		Location: &d.config.Location,
 	})
 	if err != nil {
 		return err
 	}
 
-	err = d.deploySubscription(ctx)
+	// deploy action groups
+	err = d.deployRPSubscription(ctx)
 	if err != nil {
 		return err
 	}
 
-	err = d.deployManagedIdentity(ctx)
+	// deploy managed identity
+	err = d.deployRPManagedIdentity(ctx)
 	if err != nil {
 		return err
 	}
 
-	msi, err := d.userassignedidentities.Get(ctx, d.config.ResourceGroupName, "aro-rp-"+d.config.Location)
+	rpMSI, err := d.userassignedidentities.Get(ctx, d.config.RPResourceGroupName, "aro-rp-"+d.config.Location)
 	if err != nil {
 		return err
 	}
@@ -81,19 +83,20 @@ func (d *deployer) PreDeploy(ctx context.Context) error {
 	// Due to https://github.com/Azure/azure-resource-manager-schemas/issues/1067
 	// we can't use conditions to define ACR replication object deployment.
 	if d.config.Configuration.ACRReplicaDisabled != nil && !*d.config.Configuration.ACRReplicaDisabled {
-		err = d.deployGloalACRReplication(ctx)
+		err = d.deployRPGlobalACRReplication(ctx)
 		if err != nil {
 			return err
 		}
 	}
 
-	err = d.deployGlobal(ctx, msi.PrincipalID.String())
+	// deploy ACR RBAC, RP version storage account
+	err = d.deployRPGlobal(ctx, rpMSI.PrincipalID.String())
 	if err != nil {
 		return err
 	}
 
 	// deploy NSGs, keyvaults
-	err = d.deployPreDeploy(ctx, msi.PrincipalID.String())
+	err = d.deployRPPreDeploy(ctx, rpMSI.PrincipalID.String())
 	if err != nil {
 		return err
 	}
@@ -141,7 +144,7 @@ func (d *deployer) enableEncryptionAtHostSubscriptionFeatureFlag(ctx context.Con
 	return err
 }
 
-func (d *deployer) deployGlobal(ctx context.Context, rpServicePrincipalID string) error {
+func (d *deployer) deployRPGlobal(ctx context.Context, rpServicePrincipalID string) error {
 	deploymentName := "rp-global-" + d.config.Location
 
 	b, err := Asset(generator.FileRPProductionGlobal)
@@ -170,7 +173,7 @@ func (d *deployer) deployGlobal(ctx context.Context, rpServicePrincipalID string
 	})
 }
 
-func (d *deployer) deployGloalACRReplication(ctx context.Context) error {
+func (d *deployer) deployRPGlobalACRReplication(ctx context.Context) error {
 	deploymentName := "rp-global-acr-replication-" + d.config.Location
 
 	b, err := Asset(generator.FileRPProductionGlobalACRReplication)
@@ -199,7 +202,7 @@ func (d *deployer) deployGloalACRReplication(ctx context.Context) error {
 	})
 }
 
-func (d *deployer) deployGlobalSubscription(ctx context.Context) error {
+func (d *deployer) deployRPGlobalSubscription(ctx context.Context) error {
 	deploymentName := "rp-global-subscription-" + d.config.Location
 
 	b, err := Asset(generator.FileRPProductionGlobalSubscription)
@@ -239,7 +242,7 @@ func (d *deployer) deployGlobalSubscription(ctx context.Context) error {
 	return nil
 }
 
-func (d *deployer) deploySubscription(ctx context.Context) error {
+func (d *deployer) deployRPSubscription(ctx context.Context) error {
 	deploymentName := "rp-production-subscription-" + d.config.Location
 
 	b, err := Asset(generator.FileRPProductionSubscription)
@@ -262,7 +265,7 @@ func (d *deployer) deploySubscription(ctx context.Context) error {
 	})
 }
 
-func (d *deployer) deployManagedIdentity(ctx context.Context) error {
+func (d *deployer) deployRPManagedIdentity(ctx context.Context) error {
 	deploymentName := "rp-production-managed-identity"
 
 	b, err := Asset(generator.FileRPProductionManagedIdentity)
@@ -277,7 +280,7 @@ func (d *deployer) deployManagedIdentity(ctx context.Context) error {
 	}
 
 	d.log.Infof("deploying %s", deploymentName)
-	return d.deployments.CreateOrUpdateAndWait(ctx, d.config.ResourceGroupName, deploymentName, mgmtfeatures.Deployment{
+	return d.deployments.CreateOrUpdateAndWait(ctx, d.config.RPResourceGroupName, deploymentName, mgmtfeatures.Deployment{
 		Properties: &mgmtfeatures.DeploymentProperties{
 			Template: template,
 			Mode:     mgmtfeatures.Incremental,
@@ -285,11 +288,11 @@ func (d *deployer) deployManagedIdentity(ctx context.Context) error {
 	})
 }
 
-func (d *deployer) deployPreDeploy(ctx context.Context, rpServicePrincipalID string) error {
+func (d *deployer) deployRPPreDeploy(ctx context.Context, rpServicePrincipalID string) error {
 	deploymentName := "rp-production-predeploy"
 
 	var isCreate bool
-	_, err := d.deployments.Get(ctx, d.config.ResourceGroupName, deploymentName)
+	_, err := d.deployments.Get(ctx, d.config.RPResourceGroupName, deploymentName)
 	if isDeploymentNotFoundError(err) {
 		isCreate = true
 		err = nil
@@ -318,7 +321,7 @@ func (d *deployer) deployPreDeploy(ctx context.Context, rpServicePrincipalID str
 	}
 
 	d.log.Infof("deploying %s", deploymentName)
-	return d.deployments.CreateOrUpdateAndWait(ctx, d.config.ResourceGroupName, deploymentName, mgmtfeatures.Deployment{
+	return d.deployments.CreateOrUpdateAndWait(ctx, d.config.RPResourceGroupName, deploymentName, mgmtfeatures.Deployment{
 		Properties: &mgmtfeatures.DeploymentProperties{
 			Template:   template,
 			Mode:       mgmtfeatures.Incremental,
diff --git a/pkg/deploy/upgrade.go b/pkg/deploy/upgrade.go
index 4ac4fc956..415b62152 100644
--- a/pkg/deploy/upgrade.go
+++ b/pkg/deploy/upgrade.go
@@ -9,107 +9,13 @@ import (
 	"net/url"
 	"time"
 
-	mgmtcompute "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2020-06-01/compute"
 	mgmtstorage "github.com/Azure/azure-sdk-for-go/services/storage/mgmt/2019-04-01/storage"
 	azstorage "github.com/Azure/azure-sdk-for-go/storage"
 	"github.com/Azure/go-autorest/autorest/date"
-	"github.com/Azure/go-autorest/autorest/to"
-	"k8s.io/apimachinery/pkg/util/wait"
 )
 
-const (
-	vmssPrefix = "rp-vmss-"
-)
-
-func (d *deployer) Upgrade(ctx context.Context) error {
-	timeoutCtx, cancel := context.WithTimeout(ctx, 40*time.Minute)
-	defer cancel()
-	err := d.waitForRPReadiness(timeoutCtx, vmssPrefix+d.version)
-	if err != nil {
-		return err
-	}
-
-	err = d.removeOldScalesets(ctx)
-	if err != nil {
-		return err
-	}
-
-	// Must be last step so we can be sure there are no RPs at older versions still serving
-	return d.saveRPVersion(ctx)
-}
-
-func (d *deployer) waitForRPReadiness(ctx context.Context, vmssName string) error {
-	scalesetVMs, err := d.vmssvms.List(ctx, d.config.ResourceGroupName, vmssName, "", "", "")
-	if err != nil {
-		return err
-	}
-
-	d.log.Printf("waiting for %s instances to be healthy", vmssName)
-	return wait.PollImmediateUntil(10*time.Second, func() (bool, error) {
-		for _, vm := range scalesetVMs {
-			r, err := d.vmssvms.GetInstanceView(ctx, d.config.ResourceGroupName, vmssName, *vm.InstanceID)
-			if err != nil || *r.VMHealth.Status.Code != "HealthState/healthy" {
-				d.log.Printf("instance %s status %s", *vm.InstanceID, *r.VMHealth.Status.Code)
-				return false, nil
-			}
-		}
-
-		return true, nil
-	}, ctx.Done())
-}
-
-func (d *deployer) removeOldScalesets(ctx context.Context) error {
-	d.log.Print("removing old scalesets")
-	scalesets, err := d.vmss.List(ctx, d.config.ResourceGroupName)
-	if err != nil {
-		return err
-	}
-
-	for _, vmss := range scalesets {
-		if *vmss.Name == vmssPrefix+d.version {
-			continue
-		}
-
-		err = d.removeOldScaleset(ctx, *vmss.Name)
-		if err != nil {
-			return err
-		}
-	}
-
-	return nil
-}
-
-func (d *deployer) removeOldScaleset(ctx context.Context, vmssName string) error {
-	scalesetVMs, err := d.vmssvms.List(ctx, d.config.ResourceGroupName, vmssName, "", "", "")
-	if err != nil {
-		return err
-	}
-
-	d.log.Printf("stopping scaleset %s", vmssName)
-	errors := make(chan error, len(scalesetVMs))
-	for _, vm := range scalesetVMs {
-		go func(id string) {
-			errors <- d.vmssvms.RunCommandAndWait(ctx, d.config.ResourceGroupName, vmssName, id, mgmtcompute.RunCommandInput{
-				CommandID: to.StringPtr("RunShellScript"),
-				Script:    &[]string{"systemctl stop aro-rp"},
-			})
-		}(*vm.InstanceID) // https://golang.org/doc/faq#closures_and_goroutines
-	}
-
-	d.log.Print("waiting for instances to stop")
-	for range scalesetVMs {
-		err := <-errors
-		if err != nil {
-			return err
-		}
-	}
-
-	d.log.Printf("deleting scaleset %s", vmssName)
-	return d.vmss.DeleteAndWait(ctx, d.config.ResourceGroupName, vmssName)
-}
-
-// saveRPVersion for current location in shared storage account for environment
-func (d *deployer) saveRPVersion(ctx context.Context) error {
+// SaveVersion for current location in shared storage account for environment
+func (d *deployer) SaveVersion(ctx context.Context) error {
 	d.log.Printf("saving rpVersion %s deployed in %s to storage account %s", d.version, d.config.Location, *d.config.Configuration.RPVersionStorageAccountName)
 	t := time.Now().UTC().Truncate(time.Second)
 	res, err := d.globalaccounts.ListAccountSAS(
diff --git a/pkg/deploy/upgrade_rp.go b/pkg/deploy/upgrade_rp.go
new file mode 100644
index 000000000..7359bdad1
--- /dev/null
+++ b/pkg/deploy/upgrade_rp.go
@@ -0,0 +1,98 @@
+package deploy
+
+// Copyright (c) Microsoft Corporation.
+// Licensed under the Apache License 2.0.
+
+import (
+	"context"
+	"time"
+
+	mgmtcompute "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2020-06-01/compute"
+	"github.com/Azure/go-autorest/autorest/to"
+	"k8s.io/apimachinery/pkg/util/wait"
+)
+
+const (
+	rpVMSSPrefix = "rp-vmss-"
+)
+
+func (d *deployer) UpgradeRP(ctx context.Context) error {
+	timeoutCtx, cancel := context.WithTimeout(ctx, 40*time.Minute)
+	defer cancel()
+	err := d.rpWaitForReadiness(timeoutCtx, rpVMSSPrefix+d.version)
+	if err != nil {
+		return err
+	}
+
+	return d.rpRemoveOldScalesets(ctx)
+}
+
+func (d *deployer) rpWaitForReadiness(ctx context.Context, vmssName string) error {
+	scalesetVMs, err := d.vmssvms.List(ctx, d.config.RPResourceGroupName, vmssName, "", "", "")
+	if err != nil {
+		return err
+	}
+
+	d.log.Printf("waiting for %s instances to be healthy", vmssName)
+	return wait.PollImmediateUntil(10*time.Second, func() (bool, error) {
+		for _, vm := range scalesetVMs {
+			r, err := d.vmssvms.GetInstanceView(ctx, d.config.RPResourceGroupName, vmssName, *vm.InstanceID)
+			if err != nil || *r.VMHealth.Status.Code != "HealthState/healthy" {
+				d.log.Printf("instance %s status %s", *vm.InstanceID, *r.VMHealth.Status.Code)
+				return false, nil
+			}
+		}
+
+		return true, nil
+	}, ctx.Done())
+}
+
+func (d *deployer) rpRemoveOldScalesets(ctx context.Context) error {
+	d.log.Print("removing old scalesets")
+	scalesets, err := d.vmss.List(ctx, d.config.RPResourceGroupName)
+	if err != nil {
+		return err
+	}
+
+	for _, vmss := range scalesets {
+		if *vmss.Name == rpVMSSPrefix+d.version {
+			continue
+		}
+
+		err = d.rpRemoveOldScaleset(ctx, *vmss.Name)
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func (d *deployer) rpRemoveOldScaleset(ctx context.Context, vmssName string) error {
+	scalesetVMs, err := d.vmssvms.List(ctx, d.config.RPResourceGroupName, vmssName, "", "", "")
+	if err != nil {
+		return err
+	}
+
+	d.log.Printf("stopping scaleset %s", vmssName)
+	errors := make(chan error, len(scalesetVMs))
+	for _, vm := range scalesetVMs {
+		go func(id string) {
+			errors <- d.vmssvms.RunCommandAndWait(ctx, d.config.RPResourceGroupName, vmssName, id, mgmtcompute.RunCommandInput{
+				CommandID: to.StringPtr("RunShellScript"),
+				Script:    &[]string{"systemctl stop aro-rp"},
+			})
+		}(*vm.InstanceID) // https://golang.org/doc/faq#closures_and_goroutines
+	}
+
+	d.log.Print("waiting for instances to stop")
+	for range scalesetVMs {
+		err := <-errors
+		if err != nil {
+			return err
+		}
+	}
+
+	d.log.Printf("deleting scaleset %s", vmssName)
+	return d.vmss.DeleteAndWait(ctx, d.config.RPResourceGroupName, vmssName)
+}
diff --git a/pkg/portal/ssh/proxy.go b/pkg/portal/ssh/proxy.go
index df736e5ae..a25e21b49 100644
--- a/pkg/portal/ssh/proxy.go
+++ b/pkg/portal/ssh/proxy.go
@@ -139,7 +139,7 @@ func (s *ssh) newConn(ctx context.Context, c1 net.Conn) error {
 		return err
 	}
 
-	address := fmt.Sprintf("%s:%d", openShiftDoc.OpenShiftCluster.Properties.NetworkProfile.PrivateEndpointIP, 2200+portalDoc.Portal.SSH.Master)
+	address := fmt.Sprintf("%s:%d", openShiftDoc.OpenShiftCluster.Properties.NetworkProfile.APIServerPrivateEndpointIP, 2200+portalDoc.Portal.SSH.Master)
 
 	c2, err := s.dialer.DialContext(ctx, "tcp", address)
 	if err != nil {
diff --git a/pkg/util/restconfig/restconfig.go b/pkg/util/restconfig/restconfig.go
index d0082423d..c37905eeb 100644
--- a/pkg/util/restconfig/restconfig.go
+++ b/pkg/util/restconfig/restconfig.go
@@ -22,7 +22,7 @@ func RestConfig(dialer proxy.Dialer, oc *api.OpenShiftCluster) (*rest.Config, er
 	// k8s.io/client-go/transport/cache.go, k8s caches our transport, and it
 	// can't tell if data in the restconfig.Dial closure has changed.  We don't
 	// want it to cache a transport that can never work.
-	if oc.Properties.NetworkProfile.PrivateEndpointIP == "" {
+	if oc.Properties.NetworkProfile.APIServerPrivateEndpointIP == "" {
 		return nil, errors.New("privateEndpointIP is empty")
 	}
 
@@ -56,6 +56,6 @@ func DialContext(dialer proxy.Dialer, oc *api.OpenShiftCluster) func(ctx context
 			return nil, err
 		}
 
-		return dialer.DialContext(ctx, network, oc.Properties.NetworkProfile.PrivateEndpointIP+":"+port)
+		return dialer.DialContext(ctx, network, oc.Properties.NetworkProfile.APIServerPrivateEndpointIP+":"+port)
 	}
 }


Test patch:
diff --git a/pkg/api/admin/openshiftcluster_validatestatic_test.go b/pkg/api/admin/openshiftcluster_validatestatic_test.go
index 2fdcc1b28..e1636612c 100644
--- a/pkg/api/admin/openshiftcluster_validatestatic_test.go
+++ b/pkg/api/admin/openshiftcluster_validatestatic_test.go
@@ -342,12 +342,12 @@ func TestOpenShiftClusterStaticValidateDelta(t *testing.T) {
 				return &OpenShiftCluster{
 					Properties: OpenShiftClusterProperties{
 						NetworkProfile: NetworkProfile{
-							PrivateEndpointIP: "1.2.3.4",
+							APIServerPrivateEndpointIP: "1.2.3.4",
 						},
 					},
 				}
 			},
-			modify:  func(oc *OpenShiftCluster) { oc.Properties.NetworkProfile.PrivateEndpointIP = "4.3.2.1" },
+			modify:  func(oc *OpenShiftCluster) { oc.Properties.NetworkProfile.APIServerPrivateEndpointIP = "4.3.2.1" },
 			wantErr: "400: PropertyChangeNotAllowed: properties.networkProfile.privateEndpointIp: Changing property 'properties.networkProfile.privateEndpointIp' is not allowed.",
 		},
 		{
diff --git a/pkg/portal/kubeconfig/proxy_test.go b/pkg/portal/kubeconfig/proxy_test.go
index 0642d76de..409eb1757 100644
--- a/pkg/portal/kubeconfig/proxy_test.go
+++ b/pkg/portal/kubeconfig/proxy_test.go
@@ -93,7 +93,7 @@ func TestProxy(t *testing.T) {
 	resourceID := "/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/rg/providers/microsoft.redhatopenshift/openshiftclusters/cluster"
 	username := "username"
 	token := "00000000-0000-0000-0000-000000000000"
-	privateEndpointIP := "1.2.3.4"
+	apiServerPrivateEndpointIP := "1.2.3.4"
 
 	cakey, cacerts, err := utiltls.GenerateKeyAndCertificate("ca", nil, nil, true, false)
 	if err != nil {
@@ -158,7 +158,7 @@ func TestProxy(t *testing.T) {
 					OpenShiftCluster: &api.OpenShiftCluster{
 						Properties: api.OpenShiftClusterProperties{
 							NetworkProfile: api.NetworkProfile{
-								PrivateEndpointIP: privateEndpointIP,
+								APIServerPrivateEndpointIP: apiServerPrivateEndpointIP,
 							},
 							AROServiceKubeconfig: api.SecureBytes(serviceKubeconfig),
 							AROSREKubeconfig:     api.SecureBytes(sreKubeconfig),
@@ -169,7 +169,7 @@ func TestProxy(t *testing.T) {
 				checker.AddOpenShiftClusterDocuments(openShiftClusterDocument)
 			},
 			mocks: func(dialer *mock_proxy.MockDialer) {
-				dialer.EXPECT().DialContext(gomock.Any(), "tcp", privateEndpointIP+":6443").Return(l.DialContext(ctx, "", ""))
+				dialer.EXPECT().DialContext(gomock.Any(), "tcp", apiServerPrivateEndpointIP+":6443").Return(l.DialContext(ctx, "", ""))
 			},
 			wantStatusCode: http.StatusOK,
 			wantBody:       "GET /test HTTP/1.1\r\nHost: kubernetes:6443\r\nAccept-Encoding: gzip\r\nUser-Agent: Go-http-client/1.1\r\nX-Authenticated-Name: system:aro-service\r\n\r\n",
@@ -194,7 +194,7 @@ func TestProxy(t *testing.T) {
 					OpenShiftCluster: &api.OpenShiftCluster{
 						Properties: api.OpenShiftClusterProperties{
 							NetworkProfile: api.NetworkProfile{
-								PrivateEndpointIP: privateEndpointIP,
+								APIServerPrivateEndpointIP: apiServerPrivateEndpointIP,
 							},
 							AROServiceKubeconfig: api.SecureBytes(serviceKubeconfig),
 							AROSREKubeconfig:     api.SecureBytes(sreKubeconfig),
@@ -205,7 +205,7 @@ func TestProxy(t *testing.T) {
 				checker.AddOpenShiftClusterDocuments(openShiftClusterDocument)
 			},
 			mocks: func(dialer *mock_proxy.MockDialer) {
-				dialer.EXPECT().DialContext(gomock.Any(), "tcp", privateEndpointIP+":6443").Return(l.DialContext(ctx, "", ""))
+				dialer.EXPECT().DialContext(gomock.Any(), "tcp", apiServerPrivateEndpointIP+":6443").Return(l.DialContext(ctx, "", ""))
 			},
 			wantStatusCode: http.StatusOK,
 			wantBody:       "GET /test HTTP/1.1\r\nHost: kubernetes:6443\r\nAccept-Encoding: gzip\r\nUser-Agent: Go-http-client/1.1\r\nX-Authenticated-Name: system:aro-sre\r\n\r\n",
@@ -339,7 +339,7 @@ func TestProxy(t *testing.T) {
 					OpenShiftCluster: &api.OpenShiftCluster{
 						Properties: api.OpenShiftClusterProperties{
 							NetworkProfile: api.NetworkProfile{
-								PrivateEndpointIP: privateEndpointIP,
+								APIServerPrivateEndpointIP: apiServerPrivateEndpointIP,
 							},
 						},
 					},
diff --git a/pkg/portal/prometheus/proxy_test.go b/pkg/portal/prometheus/proxy_test.go
index fb6bbbfbc..079188a13 100644
--- a/pkg/portal/prometheus/proxy_test.go
+++ b/pkg/portal/prometheus/proxy_test.go
@@ -171,7 +171,7 @@ func testKubeconfig(cacerts []*x509.Certificate, clientkey *rsa.PrivateKey, clie
 func TestProxy(t *testing.T) {
 	ctx := context.Background()
 	resourceID := "/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/rg/providers/microsoft.redhatopenshift/openshiftclusters/cluster"
-	privateEndpointIP := "1.2.3.4"
+	apiServerPrivateEndpointIP := "1.2.3.4"
 
 	cakey, cacerts, err := utiltls.GenerateKeyAndCertificate("ca", nil, nil, true, false)
 	if err != nil {
@@ -213,7 +213,7 @@ func TestProxy(t *testing.T) {
 					OpenShiftCluster: &api.OpenShiftCluster{
 						Properties: api.OpenShiftClusterProperties{
 							NetworkProfile: api.NetworkProfile{
-								PrivateEndpointIP: privateEndpointIP,
+								APIServerPrivateEndpointIP: apiServerPrivateEndpointIP,
 							},
 							AROServiceKubeconfig: api.SecureBytes(serviceKubeconfig),
 						},
@@ -224,7 +224,7 @@ func TestProxy(t *testing.T) {
 				checker.AddOpenShiftClusterDocuments(openShiftClusterDocument)
 			},
 			mocks: func(dialer *mock_proxy.MockDialer) {
-				dialer.EXPECT().DialContext(gomock.Any(), "tcp", privateEndpointIP+":6443").Return(l.DialContext(ctx, "", ""))
+				dialer.EXPECT().DialContext(gomock.Any(), "tcp", apiServerPrivateEndpointIP+":6443").Return(l.DialContext(ctx, "", ""))
 			},
 			wantStatusCode: http.StatusOK,
 			wantBody:       "GET /test HTTP/1.1\r\nHost: prometheus-k8s-0:9090\r\nAccept-Encoding: gzip\r\nUser-Agent: Go-http-client/1.1\r\n\r\n",
diff --git a/pkg/portal/ssh/proxy_test.go b/pkg/portal/ssh/proxy_test.go
index 80854f8ee..532d85019 100644
--- a/pkg/portal/ssh/proxy_test.go
+++ b/pkg/portal/ssh/proxy_test.go
@@ -141,7 +141,7 @@ func TestProxy(t *testing.T) {
 	resourceGroup := "rg"
 	resourceName := "cluster"
 	resourceID := "/subscriptions/" + subscriptionID + "/resourcegroups/" + resourceGroup + "/providers/microsoft.redhatopenshift/openshiftclusters/" + resourceName
-	privateEndpointIP := "1.2.3.4"
+	apiServerPrivateEndpointIP := "1.2.3.4"
 
 	hostKey, _, err := utiltls.GenerateKeyAndCertificate("proxy", nil, nil, false, false)
 	if err != nil {
@@ -166,7 +166,7 @@ func TestProxy(t *testing.T) {
 			OpenShiftCluster: &api.OpenShiftCluster{
 				Properties: api.OpenShiftClusterProperties{
 					NetworkProfile: api.NetworkProfile{
-						PrivateEndpointIP: privateEndpointIP,
+						APIServerPrivateEndpointIP: apiServerPrivateEndpointIP,
 					},
 					SSHKey: api.SecureBytes(x509.MarshalPKCS1PrivateKey(clusterKey)),
 				},
@@ -213,7 +213,7 @@ func TestProxy(t *testing.T) {
 				checker.AddOpenShiftClusterDocuments(openShiftClusterDocument)
 			},
 			mocks: func(dialer *mock_proxy.MockDialer) {
-				dialer.EXPECT().DialContext(gomock.Any(), "tcp", privateEndpointIP+":2201").Return(l.DialContext(ctx, "", ""))
+				dialer.EXPECT().DialContext(gomock.Any(), "tcp", apiServerPrivateEndpointIP+":2201").Return(l.DialContext(ctx, "", ""))
 			},
 			wantLogs: []map[string]types.GomegaMatcher{
 				{
@@ -367,7 +367,7 @@ func TestProxy(t *testing.T) {
 				checker.AddOpenShiftClusterDocuments(openShiftClusterDocument)
 			},
 			mocks: func(dialer *mock_proxy.MockDialer) {
-				dialer.EXPECT().DialContext(gomock.Any(), "tcp", privateEndpointIP+":2201").Return(nil, fmt.Errorf("sad"))
+				dialer.EXPECT().DialContext(gomock.Any(), "tcp", apiServerPrivateEndpointIP+":2201").Return(nil, fmt.Errorf("sad"))
 			},
 			wantErrPrefix: "EOF",
 			wantLogs: []map[string]types.GomegaMatcher{