Service Fabric : Créer un cluster via un template ARM

MAI30

Le but de cet article est de décrypter la création d’un cluster Service Fabric via des templates ARM, et comment utiliser ceux-ci pour créer des cluster multi nodes. Et par ailleurs nous verrons comment mettre en place via ARM un cluster Service Fabric accessible uniquement par des IPs privées ce qui peut être utile si vous souhaitez héberger votre cluster Service Fabric dans un Virtual Network connecté à un site to site ou à un Express Route via du private peering.

Avant de voir le contenu du template ARM, voici les types de ressources dont nous avons besoin au minima pour créer un cluster Service Fabric

  • Virtual Network
  • Storage
  • Public IP Address (si besoin)
  • Load Balancer
  • Virtual Machine Scale Set
  • Service Fabric Cluster

Dans mon cas je veux créer un cluster qui aurait cette typologie réseau suivante :

service fabric multi subnet


Ici, nous allons écrire notre template ARM en partant du début. Et comme bien souvent le début, c’est la création des couches réseaux, c’est souvent elle qui est construite en premier, car la conception de celle-ci, notamment en terme d’adressage IP doit être prévue dans le cas par exemple où on connecte ce VNET via un VPN ou via ExpressRoute, voici ci-dessous la déclaration de mon VNET :

Paramètres :

    "virtualNetwork": {
       "type": "object",
       "defaultValue": {
         "name": "",
         "addressPrefix": "",
         "subnets": [
           {
             "name": "",
             "addressPrefix": ""
           }
         ]
       },
       "metadata": {
         "description": "Virtual Network object"
       }
     },

Par convention, je remets la définition de tous mes paramètres dans les variables, ce qui me permet de faire du contrôle dessus si besoin, ici je fais juste une recopie de ceux-là, la définition de ma source sera la suivante :

    {
       "apiVersion": "2017-10-01",
       "type": "Microsoft.Network/virtualNetworks",
       "name": "[variables('virtualNetwork').name]",
       "location": "[resourceGroup().location]",
       "tags": {
         "displayName": "Virtual Network"
       },
       "properties": {
         "addressSpace": {
           "addressPrefixes": [
             "[variables('virtualNetwork').addressPrefix]"
           ]
         },
         "copy": [
           {
             "name": "subnets",
             "count": "[length(variables('virtualNetwork').subnets)]",
             "input": {
               "name": "[variables('virtualNetwork').subnets[copyIndex('subnets')].name]",
               "properties": {
                 "addressPrefix": "[variables('virtualNetwork').subnets[copyIndex('subnets')].addressPrefix]"
               }
             }
           }
         ]
       }
     },

Si vous avez lu mes derniers articles rien de bien nouveau, à part que j’ai mis les propriétés de mon Virtual Network dans un objet.

Alors bien entendu il est toujours possible d’ajouter dans ce template des définitions de NSG, des déclarations de VNet peering ou même des routes tables, mais pour cet article je n’ai pas besoin de tout ceci.

Dans mon cas d’usage je vais prendre ces paramètres pour exécuter mon script :

    "virtualNetwork": {
       "value": {
         "name": "demo-blog-arm",
         "addressPrefix": "16.0.0.0/20",
         "subnets": [
           {
             "name": "FrontSubnet",
             "addressPrefix": "16.0.0.0/24"
           },
           {
             "name": "MiddleSubnet",
             "addressPrefix": "16.0.1.0/24"
           },
           {
             "name": "BackSubnet",
             "addressPrefix": "16.0.2.0/24"
           },
           {
             "name": "AdminSubnet",
             "addressPrefix": "16.0.3.0/24"
           },
           {
             "name": "GatewaySubnet",
             "addressPrefix": "16.0.15.224/27"
           }
         ]
       }
     },

Rien de bien étonnant dans ce cas-ci, mais on notera tout de même que d’avoir mis un exemple json vide dans la defaultValue de mes objets virtualNetwork et subnets me permet facilement de compléter ce fichier quand je souhaite déployer par exemple depuis Visual Studio. Bien entendu cela peut poser des soucis si on n’est pas assez consciencieux lorsqu’on déploie nos ressources, il serait dommage de laisser une defaultValue contenant notre pseudo schéma, mais bon il s’agit là d’un palliatif au manque de ressource de type jsonObject avec un schéma..


Continuons à déclarer nos ressources, pour cela créons nos comptes de stockage. Pour Service Fabric, je vous conseille de créer au moins 2 comptes de stockage qui ont pour rôle les suivants :

  • Compte de stockage des logs Service Fabric, utile en cas de contact avec le support Microsoft
  • Compte de stockage pour les données issues d’Azure Diagnostics

Rien ne vous en empêche d’en créer plus si cela vous le dit, donc en terme de paramètres nous avons cela :

"storageAccounts": {
   "type": "array",
   "defaultValue": [
     {
       "name": ""
     }
   ],
   "metadata": {
     "description": "Storage Account list"
   }
},
"globalStorageAccountSku": {
   "type": "string",
   "allowedValues": [
     "Standard_LRS",
     "Standard_GRS"
   ],
   "metadata": {
     "description": "Sku for all storage accounts"
   }
}

Rien de bien spécifique dans notre cas, passons maintenant à la déclaration des ressources :

{
   "apiVersion": "2017-10-01",
   "type": "Microsoft.Storage/storageAccounts",
   "name": "[variables('storageAccount')[copyIndex('storageLoop')].name]",
   "location": "[resourceGroup().location]",
   "properties": {
     "encryption": {
       "keySource": "Microsoft.Storage",
       "services": {
         "blob": {
           "enabled": true
         }
       }
     },
     "supportsHttpsTrafficOnly": true
   },
   "kind": "StorageV2",
   "sku": {
     "name": "[variables('globalStorageAccountSku')]"
   },
   "copy": {
     "name": "storageLoop",
     "count": "[length(variables('storageAccount'))]"
   }
}

Comme vous pouvez le voir, par défaut j’active l’encryption rest, et bien entendu j’autorise que les appels via https. Je pourrais bien entendu rajouter des ACL pour n’autoriser que mon Virtual Network déjà créé, mais dans mon cas il faudrait que je rajoute les ips depuis lesquelles je me connecte pour pouvoir accéder à ce Storage.

Pour mon exemple, je vais uniquement créer 2 storage account, le premier pour les Azure Diagnostics, et le deuxième pour les logs Service Fabric, très utile en cas de debug bien avancé sur l’état de santé d’un cluster Service Fabric.

Pour la partie Load Balancer, je vous propose de lire un autre de mes articles qui en parle : http://blog.woivre.fr/blog/2018/2/tips-creation-de-differents-load-balancers-via-des-templates-arm 

Maintenant que les éléments liés au réseau sont créés, on va pouvoir s’attaquer à la définition de notre cluster Service Fabric. Dans mon cas, je ne vais pas créer un template ARM qui peut répondre à toutes les possibilités qu’offre Service Fabric, je vais donc partir sur les éléments suivants :

  • Cluster Windows
  • Sécurité : Utilisation de certificat et de connexion via Azure Active Directory, ce qui suppose que celui-ci soit déjà présent dans le keyvault et que les applications soient déclarées dans l’Active Directory
  • Différents types de noeuds, chaque type de noeud a son sous réseau associé


Commençons par la définition des paramètres :

    "sfCluster": {
       "type": "object",
       "defaultValue": {
         "name": "",
         "security": {
           "osAdminUserName": "",
           "level": "EncryptAndSign",
           "thumbprint": "",
           "store": "",
           "vaultUrl": "",
           "vaultResourceId": "",
           "aad": {
             "tenantId": "",
             "clusterAppId": "",
             "clientAppId": ""
           }
         },
         "managementEndpoint": {
           "Port": "19080",
           "type": "public|private",
           "public": {
             "name": ""
           },
           "private": {
             "ipAddress": ""
           }
         },
         "nodes": [
           {
             "name": "",
             "os": {
               "publisher": "",
               "offer": "",
               "sku": "",
               "version": ""
             },
             "instance": {
               "size": "",
               "count": "",
               "tier": ""
             },
             "applicationPorts": {
               "startPort": "",
               "endPort": ""
             },
             "ephemeralPorts": {
               "startPort": "",
               "endPort": ""
             },
             "fabric": {
               "tcpGatewayPort": "",
               "httpGatewayPort": ""
             },
             "isPrimary": false,
             "instanceCount": "",
             "subnetName": "",
             "loadBalancerName": ""
           }
         ],
         "diagnosticsStoreName": "",
         "supportStoreName": ""
       },
       "metadata": {
         "description": "Service Fabric definition"
       }
     },
     "osAdminPassword": {

       "type": "securestring",
       "metadata": { "description": "Admin password for VMSS"

On notera par ailleurs que j’ai mis la mot de passe dans un champs à part afin de bénéficier de la sécurité mise en place par les paramètres de type securestring.

Le template ARM quant à lui correspond à celui-ci :

    {
       "apiVersion": "2017-07-01-preview",
       "type": "Microsoft.ServiceFabric/clusters",
       "name": "[variables('sfCluster').name]",
       "location": "[resourceGroup().location]",
       "tags": {
         "displayName": "Cluster Service Fabric"
       },
       "dependsOn": [
         "storageLoop",
       ],
       "properties": {
         "addOnFeatures": [
           "DnsService",
           "RepairManager"
         ],
         "certificate": {
           "thumbprint": "[variables('sfCluster').security.thumbprint]",
           "x509StoreName": "[variables('sfCluster').security.store]"
         },
         "azureActiveDirectory": {
           "tenantId": "[variables('sfCluster').security.aad.tenantId]",
           "clusterApplication": "[variables('sfCluster').security.aad.clusterAppId]",
           "clientApplication": "[variables('sfCluster').security.aad.clientAppId]"
         },
         "diagnosticsStorageAccountConfig": {
           "storageAccountName": "[variables('sfCluster').diagnosticsStoreName]",
           "protectedAccountKeyName": "StorageAccountKey1",
           "blobEndpoint": "[reference(concat('Microsoft.Storage/storageAccounts/', variables('sfCluster').diagnosticsStoreName), '2017-10-01').primaryEndpoints.blob]" ,
           "queueEndpoint": "[reference(concat('Microsoft.Storage/storageAccounts/', variables('sfCluster').diagnosticsStoreName), '2017-10-01').primaryEndpoints.queue]" ,
           "tableEndpoint": "[reference(concat('Microsoft.Storage/storageAccounts/', variables('sfCluster').diagnosticsStoreName), '2017-10-01').primaryEndpoints.table]"
         },
         "fabricSettings": [
           {
             "parameters": [
               {
                 "name": "ClusterProtectionLevel",
                 "value": "[variables('sfCluster').security.level]"
               }
             ],
             "name": "Security"
           }
         ],
         "managementEndpoint": "[concat('https://', if(equals(variables('sfCluster').managementEndpoint.type, 'public'), reference(concat('Microsoft.Network/publicIPAddresses/', variables('sfCluster').managementEndpoint.public.name, variables('suffix').publicIPAddress), '2017-10-01').dnsSettings.fqdn, variables('sfCluster').managementEndpoint.private.ipAddress), ':', variables('sfCluster').managementEndpoint.port)]",
         "copy": [
           {
             "name": "nodeTypes",
             "count": "[length(variables('sfCluster').nodes)]",
             "input": {
               "name": "[variables('sfCluster').nodes[copyIndex('nodeTypes')].name]",
               "applicationPorts": {
                 "endPort": "[variables('sfCluster').nodes[copyIndex('nodeTypes')].applicationPorts.endPort]",
                 "startPort": "[variables('sfCluster').nodes[copyIndex('nodeTypes')].applicationPorts.startPort]"
               },
               "clientConnectionEndpointPort": "[variables('sfCluster').nodes[copyIndex('nodeTypes')].fabric.tcpGatewayPort]",
               "durabilityLevel": "Bronze",
               "ephemeralPorts": {
                 "endPort": "[variables('sfCluster').nodes[copyIndex('nodeTypes')].ephemeralPorts.endPort]",
                 "startPort": "[variables('sfCluster').nodes[copyIndex('nodeTypes')].ephemeralPorts.startPort]"
               },
               "httpGatewayEndpointPort": "[variables('sfCluster').nodes[copyIndex('nodeTypes')].fabric.httpGatewayPort]",
               "isPrimary": "[variables('sfCluster').nodes[copyIndex('nodeTypes')].isPrimary]",
               "vmInstanceCount": "[variables('sfCluster').nodes[copyIndex('nodeTypes')].instance.count]"
             }
           }
         ],
         "reliabilityLevel": "Bronze",
         "upgradeMode": "Automatic",
         "vmImage": "Windows"
       }
     }

On peut voir que j’utilise le nom de la boucle sur mes comptes de stockage au sein de mes dépendances.

Ici dans mon cas, j’ai choisi de passer les paramètres suivants :

    "sfCluster": {
       "value": {
         "name": "democluster",
         "security": {
           "osAdminUserName": "admin",
           "level": "EncryptAndSign",
           "thumbprint": "### Thumbprint de mon certificat ###",
           "store": "My",
           "vaultUrl": "### URL de mon certificat dans le Keyvault ###",
           "vaultResourceId": "### Resource ID du Vault utilisé ###",
           "aad": {
             "tenantId": "### Mon tenant Id ###",
             "clusterAppId": "### Application Id de l’application native ###",
             "clientAppId": "### Application Id de l’application Web API ###"
           }
         },
         "managementEndpoint": {
           "Port": 19080,
           "type": "private",
           "private": {
             "ipAddress": "16.3.0.4"
           }
         },
         "nodes": [
           {
             "name": "AdminNode",
             "os": {
               "publisher": "MicrosoftWindowsServer",
               "offer": "WindowsServer",
               "sku": "2012-R2-Datacenter",
               "version": "latest"
             },
             "instance": {
               "size": "Standard_D2_V2",
               "count": 3,
               "tier": "Standard"
             },
             "applicationPorts": {
               "startPort": 20000,
               "endPort": 30000
             },
             "ephemeralPorts": {
               "startPort": 49152,
               "endPort": 65534
             },
             "fabric": {
               "tcpGatewayPort": 19000,
               "httpGatewayPort": 19080
             },
             "isPrimary": true,
             "subnetName": "AdminSubnet",
             "loadBalancerName": "admin-private-lb"
           },
           {
             "name": "BackNode",
             "os": {
               "publisher": "MicrosoftWindowsServer",
               "offer": "WindowsServer",
               "sku": "2012-R2-Datacenter",
               "version": "latest"
             },
             "instance": {
               "size": "Standard_D2_V2",
               "count": 3,
               "tier": "Standard"
             },
             "applicationPorts": {
               "startPort": 20000,
               "endPort": 30000
             },
             "ephemeralPorts": {
               "startPort": 49152,
               "endPort": 65534
             },
             "fabric": {
               "tcpGatewayPort": 19000,
               "httpGatewayPort": 19080
             },
             "isPrimary": false,
             "subnetName": "BackSubnet",
             "loadBalancerName": "back-private-lb"
           },
           {
             "name": "MidNode",
             "os": {
               "publisher": "MicrosoftWindowsServer",
               "offer": "WindowsServer",
               "sku": "2012-R2-Datacenter",
               "version": "latest"
             },
             "instance": {
               "size": "Standard_D2_V2",
               "count": 3,
               "tier": "Standard"
             },
             "applicationPorts": {
               "startPort": 20000,
               "endPort": 30000
             },
             "ephemeralPorts": {
               "startPort": 49152,
               "endPort": 65534
             },
             "fabric": {
               "tcpGatewayPort": 19000,
               "httpGatewayPort": 19080
             },
             "isPrimary": false,
             "subnetName": "MiddleSubnet",
             "loadBalancerName": "middle-private-lb"
           },
           {
             "name": "FrontNode",
             "os": {
               "publisher": "MicrosoftWindowsServer",
               "offer": "WindowsServer",
               "sku": "2012-R2-Datacenter",
               "version": "latest"
             },
             "instance": {
               "size": "Standard_D2_V2",
               "count": 3,
               "tier": "Standard"
             },
             "applicationPorts": {
               "startPort": 20000,
               "endPort": 30000
             },
             "ephemeralPorts": {
               "startPort": 49152,
               "endPort": 65534
             },
             "fabric": {
               "tcpGatewayPort": 19000,
               "httpGatewayPort": 19080
             },
             "isPrimary": false,
             "subnetName": "FrontSubnet",
             "loadBalancerName": "front-public-lb"
           }
         ],
         "diagnosticsStoreName": "diagsfdemoblog",
         "supportStoreName": "logssfdemoblog"
       }
     }


Pour le reste, je vous renvoie vers mon Github qui contient ce template ARM, ainsi qu’un exemple de paramètres si vous souhaitez le réutiliser : https://github.com/wilfriedwoivre/demo-blog/tree/master/ARM/servicefabric-complexcluster


Remonter

Tips : Création de différents Load Balancers via des templates ARM

FÉVR23

Voici un nouvel article sur la création de ressources Azure en ARM, ici il va s’agir du Load Balancer, tout ceux qui en ont créé un via le portail Azure, savent qu’il s’agit d’une bonne heure de clic-o-drome.

Cette fois ci, on va corser la chose, je vais vous montrer comment créer :

  • Plusieurs load balancers
  • Privé ou publique
  • Plusieurs ports ouverts

On va donc partir sur ce jeu de paramètres :

"loadBalancers": {
   "value": [
     {
       "name": "front",
       "accessType": "public",
       "dns": "wwosfdemo",
       "port": [ 80, 8080, 443 ]
     },
     {
       "name": "middle",
       "accessType": "private",
       "subnetName": "MiddleSubnet",
       "port": [ 8081 ]
     },
     {
       "name": "back",
       "accessType": "private",
       "subnetName": "BackSubnet",
       "port": [ 8082 ]
     },
     {
       "name": "admin",
       "accessType": "private",
       "subnetName": "AdminSubnet",
       "port": [ 19000, 19080 ]
     }

   ]
}

Pour ceux qui l’ont reconnu, ça ressemble beaucoup à un use case complexe d’une architecture Service Fabric.

Pour créer ces différentes ressources, j’ai donc besoin de 3 types de ressources qui sont :

  • Public IP : pour pouvoir exposer mes External Load Balancer
  • Subnet (et donc Virtual Network) : pour pouvoir conserver mes Internal Load Balancers
  • Load Balancer : pour créer mes Internal et External Load Balancer

Je sépare les uses case “privés” et “publique” pour des questions de lisibilité, sachant qu’on a déjà des boucles imbriquées dans ce template, on ne va pas complexifier la chose encore une fois.

Pour la création des Virtual Network je vous renvoie à mon précédent article qui parle de ce sujet là : http://blog.woivre.fr/blog/2017/12/tips-creation-dazure-virtual-network-via-les-templates-arm #autopromo

Pour la création des adresses IP publiques, on va utiliser cette partie de template :

{
   "apiVersion": "2017-10-01",
   "type": "Microsoft.Network/publicIPAddresses",
   "condition": "[equals(variables('loadBalancers')[copyIndex('publicIpLoop')].accessType, 'public')]",
   "name": "[concat(variables('loadBalancers')[copyIndex('publicIpLoop')].name, variables('suffix').publicIPAddress)]",
   "location": "[resourceGroup().location]",
   "tags": {
     "displayName": "Public IP"
   },
   "properties": {
     "dnsSettings": {
       "domainNameLabel": "[variables('loadBalancers')[copyIndex('publicIpLoop')].dns]"
     },
     "publicIPAllocationMethod": "Dynamic"
   },
   "copy": {
     "name": "publicIpLoop",
     "count": "[length(variables('loadBalancers'))]"
   }
}

On fait donc ici un mixte entre une boucle et une condition pour créer une adresse IP publique pour chacun de nos Load balancer qui en a besoin. Par ailleurs je vous conseille de nommer vos différentes boucles dans un template ARM pour des questions de lisibilité encore une fois.

Pour les Load Balancers, rappelons rapidement la structure d’un Load Balancer qui est au minima la suivante :

{
   "apiVersion": "2017-10-01",
   "type": "Microsoft.Network/loadBalancers",
   "name": "LoadBalancer",
   "location": "[resourceGroup().location]",
   "properties": {
     "frontendIPConfigurations": [],
     "backendAddressPools": [],
     "inboundNatPools": [],
     "probes": [],
     "loadBalancingRules": []
   }
}

Commençons donc par les Load Balancers publiques :

{
   "apiVersion": "2017-10-01",
   "type": "Microsoft.Network/loadBalancers",
   "condition": "[equals(variables('loadBalancers')[copyIndex('loadBalancersLoop')].accessType, 'public')]",
   "name": "[concat(variables('loadBalancers')[copyIndex('loadBalancersLoop')].name, variables('suffix').publicLoadBalancers)]",
   "location": "[resourceGroup().location]",
   "tags": {
     "displayName": "External Load Balancer"
   },
   "dependsOn": [
     "publicIpLoop"
   ],
   "properties": {
     "frontendIPConfigurations": [
       {
         "name": "FrontEndPublicIPConfiguration",
         "properties": {
           "publicIPAddress": {
             "id": "[resourceId('Microsoft.Network/publicIPAddresses', concat(variables('loadBalancers')[copyIndex('loadBalancersLoop')].name, variables('suffix').publicIPAddress))]"
           }
         }
       }
     ],
     "backendAddressPools": [
       {
         "name": "BackEndAddressPool"
       }
     ],
     "inboundNatPools": [
       {
         "name": "BackEndNatPool",
         "properties": {
           "backendPort": 3389,
           "frontendIPConfiguration": {
             "id": "[concat(resourceId('Microsoft.Network/loadBalancers', concat(variables('loadBalancers')[copyIndex('loadBalancersLoop')].name, variables('suffix').publicLoadBalancers)), '/frontendIPConfigurations/FrontEndPublicIPConfiguration')]"
           },
           "frontendPortRangeEnd": 4500,
           "frontendPortRangeStart": 3389,
           "protocol": "Tcp"
         }
       }
     ],
     "copy": [
       {
         "name": "probes",
         "count": "[length(variables('loadBalancers')[copyIndex('loadBalancersLoop')].port)]",
         "input": {
           "name": "[concat('probe-', variables('loadBalancers')[copyIndex('loadBalancersLoop')].port[copyIndex('probes')])]",
           "properties": {
             "intervalInSeconds": 5,
             "numberOfProbes": 2,
             "port": "[variables('loadBalancers')[copyIndex('loadBalancersLoop')].port[copyIndex('probes')]]",
             "protocol": "Tcp"
           }
         }
       },
       {
         "name": "loadBalancingRules",
         "count": "[length(variables('loadBalancers')[copyIndex('loadBalancersLoop')].port)]",
         "input": {
           "name": "[concat('rule-', variables('loadBalancers')[copyIndex('loadBalancersLoop')].port[copyIndex('loadBalancingRules')])]",
           "properties": {
             "backendAddressPool": {
               "id": "[concat(resourceId('Microsoft.Network/loadBalancers', concat(variables('loadBalancers')[copyIndex('loadBalancersLoop')].name, variables('suffix').publicLoadBalancers)), '/backendAddressPools/BackEndAddressPool')]"
             },
             "backendPort": "[variables('loadBalancers')[copyIndex('loadBalancersLoop')].port[copyIndex('loadBalancingRules')]]",
             "enableFloatingIP": false,
             "frontendIPConfiguration": {
               "id": "[concat(resourceId('Microsoft.Network/loadBalancers', concat(variables('loadBalancers')[copyIndex('loadBalancersLoop')].name, variables('suffix').publicLoadBalancers)), '/frontendIPConfigurations/FrontEndPublicIPConfiguration')]"
             },
             "frontendPort": "[variables('loadBalancers')[copyIndex('loadBalancersLoop')].port[copyIndex('loadBalancingRules')]]",
             "idleTimeoutInMinutes": 5,
             "probe": {
               "id": "[concat(resourceId('Microsoft.Network/loadBalancers', concat(variables('loadBalancers')[copyIndex('loadBalancersLoop')].name, variables('suffix').publicLoadBalancers)), '/probes/', concat('probe-', variables('loadBalancers')[copyIndex('loadBalancersLoop')].port[copyIndex('loadBalancingRules')]))]"
             },
             "protocol": "Tcp"
           }
         }
       }
     ]
   },
   "copy": {
     "name": "loadBalancersLoop",
     "count": "[length(variables('loadBalancers'))]"
   }
}

On peut voir que c’est un peu verbeux puisque j’ai une boucle sur les Load Balancer, ainsi qu’une boucle sur les différents ports de chacun. Par ailleurs étant donné le fait que la partie “rules” contient uniquement des id de ressource, c’est juste un jeu de construction de template. Le tout se construit plutôt bien si vous êtes assez concentrés lors de l’écriture de celui-ci.


Pour les Load balancers privés, on repart sur le même template sauf pour les conditions, et la gestion de la FrontEndIpConfiguration qui indique un sous réseau, plutôt qu’une adresse IP Publique, comme on peut le voir ci-dessous :

"frontendIPConfigurations": [
   {
     "name": "FrontEndPrivateIPConfiguration",
     "properties": {
       "subnet": {
         "id": "[concat(resourceId('Microsoft.Network/virtualNetworks', variables('virtualNetwork').name), '/subnets/', variables('loadBalancers')[copyIndex('loadBalancersLoop')].subnetName)]"
       }
     }
   }

Le template est présent dans cet article, je ne le mets pas de suite sur Github, je vous le mettrai avec un article prochain sur la création d’un cluster Service Fabric complexe via un template ARM.


Remonter

Tips : Création d’Azure Virtual Network via les templates ARM

DÉCE22


Il existe de multiples manières de créer des ressources sur la plateforme Azure, je ne dirais pas qu’il y en a de meilleures que d’autres, elles ont chacune leurs avantages et leurs inconvénients. Pour rappel, pour créer des ressources sur Azure, vous avez la possibilité de le faire d’une des manières suivantes :

  • Via le portail Azure qui a le mérite d’être simple à utiliser et correspond très bien au besoin si vous souhaitez mettre en place une ressource Azure pour la première fois, ou pour créer une ressource à des vues de tests. Par contre, c’est peu automatisable, et je ne conseille pas de se dire “hey si je faisais ma création de ressources via un test ui automatisé”
  • Via la CLI ou du powershell qui ont le mérite de pouvoir être jouer de manière automatisée et qui permettent de créer rapidement des ressources via du code, cependant il va falloir prendre en compte les comportements lorsqu’une ressource existe déjà. Et bien entendu la méthode “je supprime si la ressource existe” puis je recrée a quelques limites.
  • Via les REST API Azure sont certes très puissantes, mais il y a du code à écrire, à maintenir, et on retrouve les mêmes désavantages qu’avec une ligne de commande.
  • Via des templates ARM qui sont en réalité une surcouche aux REST API Azure vont vous permettre de décrire votre architecture via un “simple” modèle JSON. Par ailleurs, ils gèrent nativement les ressources existantes en apportant les modifications si besoin, donc pas de soucis à se faire là dessus. Cependant ces templates sont plutôt verbeux, ce qui ne facilite pas toujours leur adoption, de plus ils ont quelques limites que nous ne verrons pas dans cet article.


Si je prends comme exemple la création d’un Virtual Network, si je veux le créer via un template ARM, soit j’exporte un template ARM depuis un VNET existant sur Azure, soit je pars d’une feuille blanche, soit je vais voir ce magnifique repo GIT de Microsoft : https://github.com/Azure/azure-quickstart-templates. Il a pour avantage de contenir toute sorte de template pour à peu près toutes les ressources Azure disponible.

Après avoir trouvé le template que je souhaite, je peux le prendre et m’en inspirer pour faire un template avec mes paramètres souhaités et divers autres changements, je vais donc avoir quelque chose de ce type :

{
     "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
     "contentVersion": "1.0.0.0",
     "parameters": {
         "vnetName": {
             "type":"string",
             "defaultValue": "demo-vnet",
             "metadata": {
                 "description" : "Virtual network name"
             }
         },
         "vnetAddressPrefix": {
             "type": "string",
             "defaultValue": "16.0.0.0/16",
             "metadata": {
                 "description": "Address prefix"
             }
         },
         "subnet1Name": {
             "type": "string",
             "defaultValue": "Front",
             "metadata": {
                 "description": "Subnet name"               
             }
         },
         "subnet1Prefix": {
             "type": "string",
             "defaultValue": "16.0.1.0/24",
             "metadata": {
                 "description": "subnet 1 prefix"
             }
         },
         "subnet2Name": {
             "type": "string",
             "defaultValue": "Back",
             "metadata": {
                 "description": "Subnet name"               
             }
         },
         "subnet2Prefix": {
             "type": "string",
             "defaultValue": "16.0.3.0/24",
             "metadata": {
                 "description": "subnet 2 prefix"
             }
         }
     },
     "variables": {
         "vnetName":"[parameters('vnetName')]",
         "vnetAddressPrefix": "[parameters('vnetAddressPrefix')]",
         "subnet1Name": "[parameters('subnet1Name')]",
         "subnet1Prefix": "[parameters('subnet1Prefix')]",
         "subnet2Name": "[parameters('subnet2Name')]",
         "subnet2Prefix": "[parameters('subnet2Prefix')]"
     },
     "resources": [
         {
             "apiVersion": "2015-06-15",
             "type": "Microsoft.Network/virtualNetworks",
             "name": "[variables('vnetName')]",
             "location": "[resourceGroup().location]",
             "properties": {
               "addressSpace": {
                 "addressPrefixes": [
                   "[variables('vnetAddressPrefix')]"
                 ]
               },
               "subnets":[
                   {
                       "name": "[variables('subnet1Name')]",
                       "properties":{
                           "addressPrefix": "[variables('subnet1Prefix')]"
                       }
                   },
                   {
                     "name": "[variables('subnet2Name')]",
                     "properties":{
                         "addressPrefix": "[variables('subnet2Prefix')]"
                     }
                 }
               ]
             }
         }
     ],
     "outputs": {
        
     }
}

Bon, comme on peut le voir, c'est plutôt verbeux... Mais pour moi là n’est pas le problème, c’est que pour rajouter un subnet à notre VNET, il faut soit passer par un autre template ARM qui ajoute des subnets, ce qui est plutôt compliqué pour les rejouer si on a ajouté 3/4 subnets entre le temps de création du VNET et la nouvelle exécution de ce template sur une autre souscription. Pour parer cela, il est possible d’utiliser des objets complexes, et des fonctions intégrées aux templates ARM, tel que la copy. Cela nous donnerait donc ce script si je l’applique sur le même principe :

{
     "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
     "contentVersion": "1.0.0.0",
     "parameters": {
         "vnetName": {
             "type":"string",
             "defaultValue": "demo-vnet",
             "metadata": {
                 "description" : "Virtual network name"
             }
         },
         "vnetAddressPrefix": {
             "type": "string",
             "defaultValue": "16.0.0.0/16",
             "metadata": {
                 "description": "Address prefix"
             }
         },
         "subnets":{
             "type": "array",
             "defaultValue": [
                 {
                     "Name": "Front",
                     "Prefix": "16.0.1.0/24"
                 },
                 {
                     "Name": "Middle",
                     "Prefix": "16.0.2.0/24"
                 },
                 {
                     "Name": "Back",
                     "Prefix": "16.0.3.0/24"
                 }
             ],
             "metadata": {
                 "description" : "Subnets list to create"
             }
         }
     },
     "variables": {
         "vnetName":"[parameters('vnetName')]",
         "vnetAddressPrefix": "[parameters('vnetAddressPrefix')]",
         "subnets": "[parameters('subnets')]"
     },
     "resources": [
         {
             "apiVersion": "2015-06-15",
             "type": "Microsoft.Network/virtualNetworks",
             "name": "[variables('vnetName')]",
             "location": "[resourceGroup().location]",
             "properties": {
               "addressSpace": {
                 "addressPrefixes": [
                   "[variables('vnetAddressPrefix')]"
                 ]
               },
               "copy": [
                   {
                       "name": "subnets",
                       "count": "[length(variables('subnets'))]",
                       "input": {
                           "name": "[variables('subnets')[copyIndex('subnets')].Name]",
                           "properties": {
                               "addressPrefix": "[variables('subnets')[copyIndex('subnets')].Prefix]"
                           }
                       }
                   }
               ]
             }
         }
     ],
     "outputs": {
        
     }
}

Alors, effectivement en terme de verbosité on ne gagne pas tant de ligne que cela, mais on a l’avantage de garder toujours le même template pour modifier notre VNET et y ajouter différents subnets.

Et sans oublier le lien des templates ARM, si vous le voulez pas copier coller  : https://github.com/wilfriedwoivre/demo-blog/tree/master/ARM/Tips%20VNET%20creation


Remonter

© Wilfried Woivré, tous droits réservés juin 2018