Custom hostnames i en template kan godt være svær at angive korrekt, da man nogen gange ikke har et custom hostname på et miljø, men andre gange har. Mange tror fejlagtig at man blot kan bruge en condition på ens hostnameBinding til at styre det, men det kan man ikke. Jeg vil i dette indlæg vise en af løsningerne til det, en nested deployment.

    {
      "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
      "contentVersion": "1.0.0.0",
      "parameters": {
        "customHostname": {
          "type": "string",
          "defaultValue": ""
        },
        "hostingPlanName": {
          "type": "string",
          "minLength": 1
        },
        "skuName": {
          "type": "string",
          "defaultValue": "S1"
        },
        "skuCapacity": {
          "type": "int",
          "defaultValue": 1
        }
      },
      "variables": {
        "appName": "[concat('webSite', uniqueString(resourceGroup().id))]",
        "nonwwwHostname": "[parameters('customHostname')]",
        "wwwHostname": "[concat('www.', parameters('customHostname'))]"
      },
      "resources": [
        {
          "apiVersion": "2018-02-01",
          "condition": "[not(empty(parameters('customHostname')))]",
          "name": "[concat(variables('appName'),'/',variables('nonwwwHostname'))]",
          "type": "Microsoft.Web/sites/hostNameBindings",
          "location": "[resourceGroup().location]",
          "properties": {
            "domainId": null,
            "hostNameType": "Verified",
            "siteName": "[concat(variables('appName'),'/',variables('nonwwwHostname'))]"
          }
        },
        //Custom www hostnames
        {
          "apiVersion": "2018-02-01",
          "condition": "[not(empty(parameters('customHostname')))]",
          "name": "[concat(variables('appName'),'/', variables('wwwHostname'))]",
          "type": "Microsoft.Web/sites/hostNameBindings",
          "location": "[resourceGroup().location]",
          "properties": {
            "domainId": null,
            "hostNameType": "Verified",
            "siteName": "[concat(variables('appName'),'/',variables('wwwHostname'))]"
          },
          "dependsOn": [
            "[concat('Microsoft.Web/sites/',concat(variables('appName'),'/hostNameBindings/',variables('nonwwwHostname')))]"
          ]
        },
        {
          "apiVersion": "2015-08-01",
          "name": "[parameters('hostingPlanName')]",
          "type": "Microsoft.Web/serverfarms",
          "location": "[resourceGroup().location]",
          "tags": {
            "displayName": "HostingPlan"
          },
          "sku": {
            "name": "[parameters('skuName')]",
            "capacity": "[parameters('skuCapacity')]"
          },
          "properties": {
            "name": "[parameters('hostingPlanName')]"
          }
        },
        {
          "apiVersion": "2015-08-01",
          "name": "[variables('appName')]",
          "type": "Microsoft.Web/sites",
          "location": "[resourceGroup().location]",
          "tags": {
            "[concat('hidden-related:', resourceGroup().id, '/providers/Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]": "Resource",
            "displayName": "Website"
          },
          "dependsOn": [
            "[resourceId('Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]"
          ],
          "properties": {
            "name": "[variables('appName')]",
            "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', parameters('hostingPlanName'))]"
          }
        },
        {
          "apiVersion": "2014-04-01",
          "name": "[concat(parameters('hostingPlanName'), '-', resourceGroup().name)]",
          "type": "Microsoft.Insights/autoscalesettings",
          "location": "[resourceGroup().location]",
          "tags": {
            "[concat('hidden-link:', resourceGroup().id, '/providers/Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]": "Resource",
            "displayName": "AutoScaleSettings"
          },
          "dependsOn": [
            "[resourceId('Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]"
          ],
          "properties": {
            "enabled": false,
            "name": "[concat(parameters('hostingPlanName'), '-', resourceGroup().name)]",
            "targetResourceUri": "[concat(resourceGroup().id, '/providers/Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]"
          }
        }
      ]
    }

Som giver følgende fejl, da vi slet ikke har angivet nogen custom hostname (den er tom i vores parameter array):

12:36:27 - Template deployment returned the following errors:

12:36:27 - 12:36:26 - Error: Code=InvalidTemplate; Message=Deployment template validation failed: 'The template resource 'webSitega4vb4sabeo74/' for type 'Microsoft.Web/sites/hostNameBindings' at line '104' and column '56' has incorrect segment lengths. A nested resource type must have identical number of segments as its resource name. A root resource type must have segment length one greater than its resource name. Please see https://aka.ms/arm-template/#resources for usage details.'.

12:36:27 - The deployment validation failed

Hvilket er mystisk, da vi jo har angivet i en condition at vi resourcen ikke skal med, hvis netop customHostname er tom

    "condition": "[not(empty(parameters('customHostname')))]",

Som sådan er det her ikke en fejl, men noget man ofte ser skrevet om på nettet i diverse hjælpe foraer, fordi folk generelt set ikke forstår detaljerne bag en ARM. Detajlen er at en deployment altid vil *validere *alle subresourcerne, også dem som er beskyttet af en condition og da vores ARM template er en stor deployment, så vil den validere alt. Der findes mange løsninger på det her. Dem som man oftest ser er:

  1. pille custom hostname ud af din ARM template, og kør den seperat i din release manager, som et seperat step, og lav her et tjek på om den skal køres
  2. smid din hostname i et array, og loop over den
  3. undlad helt at sætte custom hostnames op via ARM'en, og gør det manuelt bagefter

Nummer 1 kan jeg på en måde godt lide, men så ikke: det skaber et ekstra step i din deployment, som kan virke ugennemskuelig

  • du vil gerne have den provisioneret, men alt du provisionere er i din ARM UNDTAGEN din custom hostname. Fremfinding af custom hostname setup i din ARM, virker ikke, da den ikke er med i din ARM

Nummer 2 er meget fin, men den overlader rigtig meget til fantasien. Jeg kan godt lide at utrykke mig meget explicit i min provisionering; noget ikke alle kan lide. Hvis jeg vil have sat et root hostname op samt et www subdomæne hertil, vil jeg gerne explicit have disse to med i min ARM. Laver man et loop i ARM'en så virker det ikke så explicit. Jeg er hellere ikke fan af loop'et i ARM'en da den er meget anonym, og der kan godt gå lidt tid før man ser at det ikke bare er et hostname man prøver at sætte op men flere.

Nummer 3 er et no go i min verden når snakken falder på custom hostnames. Jeg vil altid gerne have mulighed for at provisionere sådan noget her automatisk, så jeg fx kan sætte nye miljøer op.

Nested templates

Min nuværende løsning er lidt kringlet, hvis man ikke er vant til at læse en ARM template, men jeg bruger en nested deployment template. Tager vi udgangspunkt i min oprindelige ARM ovenover, fjerner de to hostnameBindings og i stedet tilføjer:

        {
          "type": "Microsoft.Resources/deployments",
          "apiVersion": "2019-10-01",
          "condition": "[not(empty(parameters('customHostname')))]",
          "name": "customHostnameDeploy",
          "dependsOn": [
            "[variables('appName')]"
          ],
          "properties": {
            "mode": "Incremental",
            "expressionEvaluationOptions": {
              "scope": "outer"
            },
            "template": {
              "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
              "contentVersion": "1.0.0.0",
              "resources": [
                {
                  "apiVersion": "2018-02-01",
                  "condition": "[not(empty(parameters('customHostname')))]",
                  "name": "[concat(variables('appName'),'/',variables('nonwwwHostname'))]",
                  "type": "Microsoft.Web/sites/hostNameBindings",
                  "location": "[resourceGroup().location]",
                  "properties": {
                    "domainId": null,
                    "hostNameType": "Verified",
                    "siteName": "[concat(variables('appName'),'/',variables('nonwwwHostname'))]"
                  }
                },
                //Custom www hostnames
                {
                  "apiVersion": "2018-02-01",
                  "condition": "[not(empty(parameters('customHostname')))]",
                  "name": "[concat(variables('appName'),'/', variables('wwwHostname'))]",
                  "type": "Microsoft.Web/sites/hostNameBindings",
                  "location": "[resourceGroup().location]",
                  "properties": {
                    "domainId": null,
                    "hostNameType": "Verified",
                    "siteName": "[concat(variables('appName'),'/',variables('wwwHostname'))]"
                  },
                  "dependsOn": [
                    "[concat('Microsoft.Web/sites/',concat(variables('appName'),'/hostNameBindings/',variables('nonwwwHostname')))]"
                  ]
                }
              ]
            }
          }
        }

Får vi en fin validering

13:08:06 - VERBOSE: 13:08:06 - Template is valid.

Valideringsmotoren tjekker ikke min sub deployment, da den nu faktisk validere på baggrund af om condition er opfyldt.

Hvorfor den gør det netop med sub deployments og ikke med standard resourcer, det ved jeg ikke. Jeg har aldrig fundet svaret.

Jeg kan godt lide denne løsning:

  1. min provisionering er samlet - hostname, webapp
  2. den er explicit - jeg vil gerne have to hostnames

Har man meget store løsninger bliver man nød til at hive det ud i som moduler, og her kommer linked ARM templates til sin ret. Her kunne man hive hostnameBindings ud for sig, og ikke bruge en nested template, men en linked ARM deployment, og stadig have condition på. Her ligger det ikke samlet mere, men når en løsning bliver meget stor, vokser ARM'en hurtigt, og så giver det mening at have det hele splittet op, dog kommer condition stadig til sin ret, og vil blive valideret korrekt, så man ikke får valideringsfejl.