JUIN 18

Nouveau blog

Et voilà, j’ai encore migré mon blog. En même temps ça ne sera que la 2ème fois depuis sa création.

Petit historique pour ceux qui ne savent pas :

  • Création en 2009 sur un Wordpress hébergé gratuitement sur leur plateforme
  • Migration en 2012 vers Azure avec une solution basée sur les services suivants :
    • Azure Table Storage : pour les métadonnées des articles
    • Azure Blob Storage : pour les articles et les images
    • Azure CDN : parce que plus vite c’est toujours mieux
    • Azure Cloud Services : pour l’hébergement des services, les Web Apps n’existaient pas à l’époque
    • Le tout réalisé en full custom en C# par mes soins
    • Et une absence totale de backend, j’alimentais les articles via Open Live Writer.
  • Nouveau blog basé sur Jekyll et hébergé sur Github

Alors la seule constante dans ces 3 moutures de blog c’est bien entendu que le design est toujours aussi peu travaillé.

J’ai migré sur cette nouvelle mouture pour plusieurs raisons :

  • Arrêter d’utiliser Open Live Writer, c’est un bon produit, mais c’est un client lourd, et qui tourne que sur Windows
  • Plus de code à gérer lorsque je veux implémenter une nouvelle fonctionnalité ou faire un fix rapide.
  • Fini les galères à colorer le code, le Markdown c’est bien plus simple qu’une extension Visual Studio qui génère un HTML approximatif.

Et la dernière raison, qui est celle qui m’a fait mettre cette migration en priorité, c’est qu’à partir de début juillet 2018 je ne serai plus MVP Azure. J’ai perdu le titre car j’ai clairement un manque de contribution public ces dernières années où je n’ai pas assez fait la part des choses entre mon investissement privé chez mes clients, et mes investissements publics.

Cependant ceux qui me suivent, savent qu’en ce moment j’écris des articles et je suis assez régulièrement en meetup, parce que j’ai envie de continuer l’aventure en tant que MVP. Pour moi cette distinction apporte énormément aussi bien en terme professionnel que personnel, je vais donc continuer à partager ce que je sais au sein d’évènements communautaires et tâcher de récupérer ce titre.

JUIN 06

Service Fabric - Déployer sur un cluster multi-nodes

Lorsque vous créez des clusters Service Fabric il est possible de mettre en place plusieurs types de nœuds. Il y a plusieurs raisons pour lesquelles vous voulez faire ça qui peuvent être entre autre les suivantes :

  • Isoler les nœuds primaires contenant les services dédiées Service Fabric
  • Mettre en place une séparation réseau entre vos différents services
  • Avoir plusieurs configurations de serveurs : Beaucoup de CPU, Beaucoup de RAM, GPU,…

A partir du moment où vous avez créé plusieurs nodes types, il est fortement conseillé de mieux maitriser le déploiement de vos applications sur votre cluster afin qu’elles ne soient pas déployées n’importe où.

Prenons par exemple, un exemple de cluster respectant cette configuration suivante :

  • 2 nodes types
    • AdminNode : Nœuds uniquement pour les services fournis par Microsoft
    • AppNode : Nœuds  pour héberger les services que je développe
  • 1 point d’entrée par type de nœuds.

Passons à notre application, si je prends un service stateless issu du template Visual Studio et que je le déploie tel quel sur mon cluster, j’aurais une valeur d’instance count à –1, c’est-à-dire qu’il sera présent sur tous les noeuds de mon cluster comme on peut le voir ci dessous:

image

Si par ailleurs je provisionne uniquement 3 instances, je n’aurais pas la certitude que mon application soit présente uniquement sur les 3 serveurs qui m’intéressent dans ce cas précis.

Je vais donc rajouter des contraintes de placement à mon application afin d’être sûr qu’elle se déploie uniquement sur les noeuds de type AppNode, pour cela dans le fichier ApplicationManifest.xml, je rajoute la contrainte suivante :

    <Service Name="Web" ServicePackageActivationMode="ExclusiveProcess">  
      <StatelessService ServiceTypeName="WebType" InstanceCount="[Web_InstanceCount]">  
        <SingletonPartition />  
        <PlacementConstraints>(NodeTypeName==AppNode)</PlacementConstraints>  
      </StatelessService>  
    </Service>

Grâce à cette contrainte de placement, je m’assure que mon service se déploiera uniquement sur mon nodetype AppNode, et je peux laisser sereinement mon InstanceCount à –1.

Ici j’ai décidé de me baser sur les noms des NodeTypes puisque j’ai la maitrise de ceux-ci. Cependant il est possible de se baser sur des métriques personnalisées, pour cela il faut les déclarer sur les différents nodetypes de votre cluster. Il est possible de faire cela de deux manières :

  • A la création de votre cluster, via votre template ARM (donc aussi via un update) en ajoutant ceci à la définition de votre type :

image

  • En mettant à jour le manifest de votre cluster Service Fabric :

image

  • Via le portail Azure comme on peut le voir ci-dessous :

image

Dans un prochain article je vous montrerai comment mettre en place des contraintes de placement sur votre cluster de développement.

JUIN 04

Sandbox Azure - Gestion des groupes de ressources

Dans le cadre de la sandbox Azure dont je vous ai parlé dans les articles précédents:

On va maintenant parler de la création des Resource Groups, je vais là aussi utiliser une API qui réalisera les étapes suivantes :

  • Créer un resource group
  • Ajouter les droits à la personne qui aura réalisé la demande.
  • Stocker la demande afin de pouvoir mettre en place une expiration de celle-ci

Pour effectuer cela, j’ai besoin que mon Azure Function ait les droits pour accéder aux API de management d’Azure. Pour cela je vais utiliser la fonctionnalité des “Managed Service Identity”, celle-ci permet à mon Azure Function d’étre authentifiée en tant que Service sur mon Azure Active Directory, il suffira après de lui attribuer les droits suffisants pour pouvoir réaliser la création de mes ResourceGroups, la suppression de ceux-ci et la gestion de mes utilisateurs. Il est possible pour cela de soit créer un Custom Role qui fait ces actions, ou alors d’attribuer directement les droits Owner à la souscription.

Pour activer cette fonctionnalité, rien de plus simple dans le portail Azure, vous allez sur l’interface de votre Azure Function, puis dans “Platform Features” , il vous suffit d’aller sur Managed Service Identity et de l’activer :

image

Comme on peut le voir l’activation est assez simple à mettre en place, et bonne nouvelle cette fonctionnalité de “Managed Service identity” est disponible sur App Services et sur les machines virtuelles à ce jour.

Dans mon cas, j’utilise les Fluent Management librairies sur Azure, ce qui permet de m’affranchir de la construction des appels à l’API REST, de plus ce SDK me permet de m’authentifier à Azure via mon Service Identity précédemment créé :

AzureCredentials credentials = SdkContext.AzureCredentialsFactory.FromMSI(new MSILoginInformation(MSIResourceType.AppService), AzureEnvironment.AzureGlobalCloud);  
  
var azure = Azure  
        .Configure()  
        .WithLogLevel(HttpLoggingDelegatingHandler.Level.Basic)  
        .Authenticate(credentials)  
        .WithDefaultSubscription();

Par la suite, tous les appels que je ferai via mon objet azure seront identifiés avec le compte lié à mon Azure Function. Du coup je n’ai pas besoin de gérer les certificats ou à avoir une gestion de clé pour m’authentifier sur Azure.

await azure.ResourceGroups.Define(data.Name).WithRegion(Region.Create(data.Location)).CreateAsync()

Il est possible d’utiliser ce compte applicatif pour interroger les API du KeyVault afin de récupérer les secrets dont on a besoin :

var azureServiceTokenProvider = new AzureServiceTokenProvider();  
var keyVaultClient = new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(azureServiceTokenProvider.KeyVaultTokenCallback));  
  
var result = await keyVaultClient.GetSecretAsync($"https://mykv.vault.azure.net/secrets/{secretKey}");

En résumé les MSI c’est le moyen idéal pour que votre application communique avec votre infrastructure Azure et elle vous permet de vous affranchir de la gestion des secrets de vos comptes applicatifs.

MAI 30

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

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

AVRI 24

Sandbox Azure - Provisionnement des utilisateurs

Dans le cadre de la sandbox Azure que je vous ai introduit dans un de mes précédents articles : http://blog.woivre.fr/blog/2018/03/sandbox-azure-contexte-et-configuration-azure-active-directory

J’ai besoin que les utilisateurs créent un compte dans cette souscription Azure afin qu’ils puissent réaliser des tests dans Azure.

Je pourrais bien entendu mettre en place une synchronisation entre l’AD SOAT et un AD Azure, mais dans mon cas je souhaite donner la plus grande liberté sur Azure possible et faire en sorte que le tout soit jetable sans me poser trop de question. J’ai donc décidé de créer un compte pour chaque utilisateur souhaitant se connecter à la sandbox.

Afin de ne pas faire cela à la main, je vais utiliser les nouvelles Graph API de Microsoft et les “Converged Applications”.

Commençons donc par créer notre application, pour cela il faut aller sur le portail suivant : https://apps.dev.microsoft.com/#/appList, vous remarquerez qu’il ne s’agit pas du portail Azure, mais il est possible de trouver un lien vers ce portail dans la blade Azure Active Directory / App Registration

On va donc créer une nouvelle application que je vais appeler ici “demoblog-app”.

Afin de pouvoir utiliser cette application, je dois pouvoir m’authentifier avec, pour cela j’ai plusieurs solutions possibles qui sont les suivantes :

  • Générer un nouveau mot de passe
  • Générer un certificat afin de résoudre l’authentification

Dans mon cas, je vais mettre en place une authentification par mot de passe, puisque je ne veux pas avoir à gérer l’expiration des différents certificats à l’avenir.

Attention : Conservez bien le mot de passe qui est généré, il est impossible de le retrouver par la suite.

Je vais par la suite choisir une plateforme pour m’authentifier, ici encore j’ai plusieurs choix qui sont les suivants :

  • Web
  • Native Application
  • Web API

Ici, je vais créer une application Web, sans pour autant activer l’autorisation Implicit Flow, j’utilise ici l’url du code de mon application Azure Function que j’ai sur mon poste de développement, mais je peux mettre n’importe quelle url, car je n’ai aucune interaction avec un utilisateur. Pour des questions de simplicité, je mets cependant le plus souvent l’url où mon service est disponible, ainsi qu’une url pour le développement.

image

J’ai créé mon application, si je fais le test, je pourrais me connecter à mon service, mais je ne pourrais rien faire tant que je ne lui aurais pas octroyé de droit.

Dans mon cas, il faut que je puisse faire les tâches suivantes :

  • Lister les utilisateurs
  • Créer des utilisateurs
  • Supprimer des utilisateurs
  • Lister les groupes
  • Ajouter un utilisateur à un groupe
  • Réinitialiser le mot de passe d’un utilisateur

Pour cela, je vais avoir besoin des droits suivants :

  • Group.ReadWrite.All (Admin Only)
  • User.ReadWrite.All (Admin Only)

Si vous testez à ce moment là, vous aurez une erreur lors de vos requêtes, par manque de droits. Pour ceux qui ont déjà mis en place des applications via Azure AD, vous avez déjà vu le bouton “Grant Permission”, et bien là c’est la même chose, sauf que vous n’avez pas le bouton à ce jour.

Il vous faut donc appeler l’url suivante :

https://login.microsoftonline.com/{tenant}/adminconsent?client_id={id}&state={state}&redirect_uri={redirectUri}

Les paramètres sont les suivants :

  • Tenant : Identifiant de votre tenant, soit sous la forme d’un GUID ou sous la forme : montenant.onmicrosoft.com
  • Id : Application id que vous pouvez retrouver quand vous créez votre application
  • state (Recommandé, mais non requis) : clé utilisée pour encrypter vos token qui seront générés par la suite
  • RedirectUri : Url de redirection qui est la même que celle que vous avez renseigné plus haut

Une fois que vous aurez validé les différents droits via cette url, vous pourrez voir votre application dans le portail Azure, dans la blade Azure Active Directory > Enterprise applications, comme on peut le voir ci dessous :

image

Une fois que votre application est créée, configurée, et approuvée, on peut passer au code applicatif afin de créer notre utilisateur.

Il nous faut donc commencer par référencer les packages NuGet suivants :

  • Microsoft.Graph : Sert à manipuler la Microsoft Graph API
  • Microsoft.Identity.Client (en preview à ce jour) : Sert à générer le token correspondant à notre application

Commençons par voir comment générer notre token, le code est le suivant :

public async Task<string> GetTokenAsync()  
{  
    const string baseAuthorityUrl = "https://login.microsoftonline.com/";  
    string[] scopes = new[] { "https://graph.microsoft.com/.default" };  
    var app = new ConfidentialClientApplication(applicationId, $"{baseAuthorityUrl}{tenantId}", redirectUrl, new ClientCredential(applicationSecret), null, new TokenCache());  
    AuthenticationResult authenticationResult = await app.AcquireTokenForClientAsync(scopes);  
  
    return authenticationResult.AccessToken;  
}

Le code est assez simple, bien qu’il diffère de ce qu’on pouvait utiliser avec ADAL auparavant, mais on retrouve la même philosophie, sauf que maintenant on a une meilleure gestion du cache et des scopes que l’on veut appliquer à notre token.

Créons maintenant notre objet pour appeler la Graph API de Microsoft :

return new GraphServiceClient(new DelegateAuthenticationProvider(
async (request) =>
    {
request.Headers.Authorization = new AuthenticationHeaderValue(“Bearer”, await helper.GetTokenAsync());
    }));

On lui passe ainsi la méthode pour qu’il puisse gérer son token, et faire ainsi les appels qu’il a besoin en cas d’expiration de celui-ci.

Maintenant passons à la création de notre utilisateur, dans notre cas, il s’agira de ce code-ci :

var parts = soatEmail.Split(new[] { '.', '@' });  
  
var password = Membership.GeneratePassword(16, 4);  
User user = await graphServiceClient.Value.Users.Request().AddAsync(new User  
{  
    AccountEnabled = true,  
    DisplayName = soatEmail,  
    MailNickname = $"{parts[0]}.{parts[1]}",  
    UserPrincipalName = $"{parts[0]}.{parts[1]}@{tenant}",  
    PasswordProfile = new PasswordProfile  
    {  
        Password = password,  
        ForceChangePasswordNextSignIn = true  
    },  
    PasswordPolicies = "DisablePasswordExpiration"  
});  
  
await graphServiceClient.Value.Groups[userGroupId].Members.References.Request().AddAsync(user);

Je génère ici un mot de passe aléatoire que l’utilisateur doit changer à sa première utilisation. Je désactive par ailleurs les contraintes sur le mot de passe pour des soucis de maintenance. On peut d’ailleurs voir sur la dernière ligne que j’ajoute par la suite mon utilisateur dans le groupe “All Users” que j’utilise pour les services communs.

Le prochain article autour de cette sandbox aura pour sujet la création de ressource sur Azure via mon Azure Function. Stay tuned !