Deploy two Azure App Services to the same App Service Plan using idempotent ARM TEMPLATE

2.9k views Asked by At

PROBLEM

How to deploy two different Azure App Services to the same App Service plan when using VSTS idempotent continuous integration / continuous deployment processes.

ENVIRONMENT

  • I have written two ARM TEMPLATES each of which deploy a web application to Azure App Service.

  • In order to deploy an App Service an Service Plan must be created first.

  • The ARM TEMPLATES currently create a unique Service Plan each for each Web App.

  • I am using VSTS Release Definitions to deploy on each successful VSTS build. i.e releases are designed to be idempotent.

  • Currently each web app has its own Resource Group which includes it own App Service Plan. Ideally each web app has its own Resource Group, App Service Plan can be in its own Resource Group however (if this is possible).

The template below is an example of one of the templates used to deploy the Web App service to an App Service Plan.

It shows the creation of the App Service Plan using the naming conversion:

appname-Plan-q2dkkaaaaaaaa

This is created using:

  • Seven Character identifier "appname" defined in the ARM parameters files.
  • Resource identifier "plan".
  • Resource Group name , which comes from random named Storage Account name "q2dkkaaaaaaaa" when it was created.

i.e

"hostingPlanName": "[concat(parameters('appName'),'-Plan-', uniqueString(resourceGroup().id))]",

EXAMPLE

{
"parameters": {
    "appName": {
        "type": "string",
        "maxLength": 7,
        "metadata": {
            "description": "The name of the app that you wish to create."
        }
    },
    "appServicePlanSku": {
        "type": "string",
        "defaultValue": "Standard",
        "metadata": {
            "description": "The Service Plan SKU"
        }
    },
    "appServicePlanWorkerSize": {
        "type": "string",
        "defaultValue": "0",
        "metadata": {
            "description": "The App Service Plan Worker Size (?)"
        }
    },
    "appServicePlanSkuCode": {
        "type": "string",
        "defaultValue": "S1",
        "metadata": {
            "description": "The App Service Plan SKU Code"
        }
    },
    "appServicePlanNumWorkers": {
        "type": "string",
        "defaultValue": "2",
        "metadata": {
            "description": "The Number of App Service Workers."
        }

},
"variables": {
    "webAppName": "[concat(parameters('appName'),'-wa-', uniqueString(resourceGroup().id))]",
    "hostingPlanName": "[concat(parameters('appName'),'-Plan-', uniqueString(resourceGroup().id))]",
    "stageSlotName": "stageSlot",
    "devSlotName": "devSlot"
    }
},
"resources": [
    {
        "apiVersion": "2016-09-01",
        "name": "[variables('hostingPlanName')]",
        "type": "Microsoft.Web/serverfarms",
        "location": "[resourceGroup().location]",
        "properties": {
            "name": "[variables('hostingPlanName')]",
            "workerSizeId": "[parameters('appServicePlanWorkerSize')]",
            "numberOfWorkers": "[parameters('appServicePlanNumWorkers')]"
        },
        "sku": {
            "Tier": "[parameters('appServicePlanSku')]",
            "Name": "[parameters('appServicePlanSkuCode')]"
        },
        "dependsOn": []
    },
    {
        "apiVersion": "2015-08-01",
        "type": "Microsoft.Web/sites",
        "name": "[variables('webAppName')]",
        "location": "[resourceGroup().location]",
        "kind": "webapp",
        "tags": {
            "Environment": "production",
            "displayName": "WebAppService"
        },
        "dependsOn": [
            "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]",
        ],
        "properties": {
            "name": "[variables('webAppName')]",
            "serverFarmId": "[resourceId('Microsoft.Web/serverfarms',variables('hostingPlanName'))]"
        },
        "resources": [
            {
                "name": "slotConfigNames",
                "type": "config",
                "apiVersion": "2015-08-01",
                "dependsOn": [
                    "[resourceId('Microsoft.Web/sites', variables('webAppName'))]"
                ],
                "tags": {
                    "displayName": "slotConfigNames"
                },
                "properties": {
                    "appSettingNames": []
                }
            },
            {
                "apiVersion": "2015-08-01",
                "name": "[variables('stageSlotName')]",
                "type": "slots",
                "location": "[resourceGroup().location]",
                "dependsOn": [
                    "[resourceId('Microsoft.Web/sites', variables('webAppName'))]"],
                "properties": {},
                "resources": []
            },
            {
                "apiVersion": "2015-08-01",
                "name": "[variables('devSlotName')]",
                "type": "slots",
                "location": "[resourceGroup().location]",
                "dependsOn": [
                    "[resourceId('Microsoft.Web/sites', variables('webAppName'))]"],
                "properties": {},
                "resources": []
            }
        ]
    }
]
}

QUESTION

I am attempting to execute two ARM TEMPLATES (similar to the above example) to deploy two different Web Apps to the same Service Plan.

Its clear that both of these Web Apps must call the same central resource to ensure they both deploy to same App Service resource name and execute any changes.

  • If the App Service plan exists = deploy web app.
  • If the App Service plan does not exist = create service plan then deploy web app.
  • If the App Service plan is changed = deploy the service plan change (e.g Tier change) then deploy the web app.

Taking the environmental description above into consideration , what options do I have to get this working?

  • VSTS Global parameter in the Release Definition maybe ?
  • ARM TEMPLATES call a PowerShell script that creates the app service plan ?

Keen to follow best practice.

I hope the above is described in enough detail. Sorry if something has been missed. Thank you.

2

There are 2 answers

0
scott_lotus On BEST ANSWER

SOLUTION

The solution in my case was to create three templates:

  • Template 1 to create the app service plan. This ARM template is stored in a BLOB container and is accessible to the release pipeline via a SAS URI.
  • Template 2 to create web app A. This template uses LINKED TEMPLATE features to call and execute the shared template.
  • Template 3 to create web app B. This template uses LINKED TEMPLATE features to call and execute the shared template.

RESULT

  • Both web apps are then published to the same server farm, sharing the instances.
  • The idempotent nature of the deployment is maintained.
  • Single point of truth for any app service plan deployment and any amendments.
  • Money saved on number of server farms required.

EXAMPLE

Shared Service Plan - ARM Template example of a shared service plan:

{
"$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
    "planLabel": {
        "defaultValue": "shared-service-plan",
        "type": "string"
    }
},
"variables": {
    "servicePlanName": "[concat(parameters('planLabel'),'-Plan-', uniqueString(resourceGroup().id))]"
},
"resources": [
    {
        "comments": "Creates an App Service Plan on the Standard (S1) SKU.",
        "type": "Microsoft.Web/serverfarms",
        "sku": {
            "name": "S1",
            "tier": "Standard",
            "size": "S1",
            "family": "S",
            "capacity": 2
        },
        "kind": "app",
        "name": "[variables('servicePlanName')]",
        "apiVersion": "2016-09-01",
        "location": "[resourceGroup().location]",
        "properties": {
            "name": "[variables('servicePlanName')]"
        },
        "dependsOn": []
    }
],
"outputs": {
    "servicePlanResourceId": {
        "type": "string",
        "value": "[resourceId('Microsoft.Web/serverfarms', variables('servicePlanName'))]"
    },
    "servicePlanName":{
        "type": "string",
        "value": "[variables('servicePlanName')]"
    },
    "resourceGroupName":{
        "type": "string",
        "value": "[resourceGroup().name]"
    }
}
}

Web App A - ARM Template Example containing LINKED TEMPLATE:

{
"$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
    "servicePlanLabel": {
        "type": "string",
        "metadata": {
            "description": "The base name for the App Service Plan to be used in the linked template."
        },
        "defaultValue": "plan"
    },
    "appServicePlanResourceGroup": {
        "type": "string",
        "metadata": {
            "Description": "The name of the Resource Group the shared App Service Plan will be deployed to."
        },
        "defaultValue": "group"
    },
    "appServicePlanTemplateUri": {
        "type": "string",
        "metadata": {
            "description": "The URI to the App Service Plan linked template in BLOB"
        }
    }
},
"variables": {},
"resources": [
    {
        "apiVersion": "2017-05-10",
        "name": "appServicePlanTemplate",
        "type": "Microsoft.Resources/deployments",
        "resourceGroup": "[parameters('appServicePlanResourceGroup')]",
        "properties": {
            "mode": "Incremental",
            "templateLink": {
                "uri": "[parameters('appServicePlanTemplateUri')]",
                "contentVersion": "1.0.0.0"
            },
            "parameters": {
                "planLabel": {
                    "value": "[parameters('servicePlanLabel')]"
                }
            }
        }
    },
    {
        "apiVersion": "2015-08-01",
        "type": "Microsoft.Web/sites",
        "name": "[variables('webAppName')]",
        "location": "[resourceGroup().location]",
        "kind": "webapp",
        "tags": {
            "Environment": "production",
            "displayName": "App"
        },
        "dependsOn": [
            "[resourceId(parameters('appServicePlanResourceGroup'), 'Microsoft.Resources/deployments', 'appServicePlanTemplate')]"
        ],
        "properties": {}
    }
}
}

Hope this is useful to someone.

Thanks Scott

REF https://github.com/MicrosoftDocs/azure-docs/blob/master/articles/azure-resource-manager/resource-group-linked-templates.md

4
Amor On

Your requirement is default supported by ARM template. Basically if an ARM template encounters a resource which is exist, it will update the resource if the properties do not match. Otherwise it will create the resource using the properties which you set in ARM template.