OCTO 12

Générer un token Azure AD via les REST API

Dernièrement j’ai eu le besoin d’accéder à un compte de stockage via un compte applicatif, j’ai donc mis en plac un SPN avec un droit RBAC sur mon compte de stockage, comme je le montre dans cet article : http://blog.woivre.fr/blog/2018/09/connectez-vous-a-vos-comptes-de-stockage-via-azure-active-directory

Maintenant il faut que je génère mon token d’accès, chose qui est assez simple avec les différentes librairies ADAL, cependant pour mon besoin j’avais les contraintes suivantes :

  • Compte applicatif avec une authentification par certificat
  • Pas de librairies supplémentaires (adieu ADAL)
  • Powershell

Première étape, il faut générer un token JWT. Pour rappel un token JWT répond à la structure suivante : base64(header).base64(payload).base64(signature)

Commençons par la construction de notre header, pour cela il nous faut le hash de notre certificat, que l’on peut récupérer de la manière suivante :

$cert = Get-Item Cert:\CurrentUser\My\$ThumbprintValue

$hash = $cert.GetCertHash()
$hashValue = [System.Convert]::ToBase64String($hash)  -replace '\+','-' -replace '/','_' -replace '='

Il est maintenant possible de constuire notre header de la manière suivante, ainsi que notre payload :

[hashtable]$header = @{alg = 'RS256'; typ= "JWT"; x5t = $thumprintValue}
[hashtable]$payload = @{aud = "https://login.microsoftonline.com/$TenantUrl/oauth2/token"; iss = $applicationId; sub=$applicationId; jti = "22b3bb26-e046-42df-9c96-65dbd72c1c81"; exp = $exp; nbf= 1536160449}

Maintenant qu’on a toutes les informations, il faut générer notre signature, et construire notre token

$headerjson = $header | ConvertTo-Json -Compress
$payloadjson = $payload | ConvertTo-Json -Compress

$headerjsonbase64 = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($headerjson)) -replace '\+','-' -replace '/','_' -replace '='
$payloadjsonbase64 = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($payloadjson)) -replace '\+','-' -replace '/','_' -replace '='

$jwt = $headerjsonbase64 + "." + $payloadjsonbase64
$toSign = [System.Text.Encoding]::UTF8.GetBytes($jwt)

$Signature = [Convert]::ToBase64String($rsa.SignData($toSign,[Security.Cryptography.HashAlgorithmName]::SHA256,[Security.Cryptography.RSASignaturePadding]::Pkcs1)) -replace '\+','-' -replace '/','_' -replace '='

$token = "$headerjsonbase64.$payloadjsonbase64.$Signature"

A noter qu’il est possible de valider la création de votre jeton JWT sur des sites comme celui-ci : https://jwt.io/

Et voilà nous avons notre token JWT qui nous servira à avoir notre access Token qu’on va pouvoir récupérer de la manière suivante:


$url = "https://login.microsoftonline.com/$TenantUrl/oauth2/token"
$body = "resource=https%3A%2F%2F$storageAccountName.blob.core.windows.net%2F&client_id=$applicationId&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer&client_assertion=$token&grant_type=client_credentials"
$responseToken = Invoke-WebRequest -Method POST -ContentType "application/x-www-form-urlencoded"  -Headers @{"accept"="application/json"} -Body $body $url -Verbose

$accessToken = ($responseToken.Content | ConvertFrom-Json).access_token

Après avoir générer notre token, il est possible de l’utiliser dans nos headers pour appeler les REST API de notre storage.

$headerSMA =  @{"Authorization" = "Bearer " + $accessToken; "x-ms-version" = "2017-11-09"}
Invoke-WebRequest -Headers $headerSMA -Method GET "https://$storageAccountName.blob.core.windows.net/$containerName/$blobName"  -OutFile $outFile

Et voilà comment appeler des API Azure tout en s’affranchissant d’ADAL. Même si on est d’accord créer notre Token avec ADAL c’est beaucoup plus simple. Et surtout moins long à lire.


OCTO 05

Créer vos groupes de ressources via un template ARM

La création des groupes de ressources se fait généralement par le portail Azure, ou via votre terminal préféré (CLI ou PowerShell). Il est posssible dorénavant de créer vos groupes de ressources via un template ARM.

Prenons un exemple de template ARM pour créer notre resource group :

{
    "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {},
    "variables": {
        "location": "West Europe",
        "name": "rg-test"
    },
    "resources": [
        {
            "type": "Microsoft.Resources/resourceGroups",
            "apiVersion": "2018-05-01",
            "location": "[variables('location')]",
            "name": "[variables('name')]",
            "properties": {}
        }
    ],
    "outputs": {}
}

Maintenant pour déployer ce template ARM, il est possible d’utiliser la commande suivante :

New-AzureRmResourceGroupDeployment -Name deploy-rg -TemplateFile .\azuredeploy.json -ResourceGroupName existing-rg

Le problème c’est qu’il faut déjà avoir un resource group dans notre souscription, ce qui n’est pas top pour initier une souscription. Mais depuis peu, il est possible d’utiliser la commande suivante :

New-AzureRmDeployment -Name deploy-rg -TemplateFile .\azuredeploy.json

Il est bien entendu possible de retrouver les déploiements passés via la commande

Get-AzureRmDeployment

Ou alors vous pouvez retrouver vos déploiements dans la blade Souscription dans le portail Azure.

Happy deploy !


SEPT 25

Connectez vous à vos comptes de Stockage via Azure Active Directory

On a vu dans un précédent article comment utiliser le KeyVault pour générer des SAS Keys : http://blog.woivre.fr/blog/2018/09/generer-des-cles-sas-pour-vos-storage-grace-a-keyvault

Dans la même philosophie que ce dernier, il est possible de s’affranchir totalement des clés de Storage dans vos configs ou votre code source, même si j’espère que pour ce dernier c’est déjà le cas.

Certains services sur Azure supportent des rôles RBAC sur les données, comme notamment les Storage Accounts, qui contiennent des droits Reader ou Contributor sur les Blob et les Queues.

Grâce à cela il est possible pour un compte AD spécifique de se connecter à mon Stockage Azure pour récupérer un fichier par exemple.

Les noms des rôles build-in qui existent autour de la donnée sont les suivants :

  • Storage Blob Data Contributor (Preview)
  • Storage Blob Data Reader (Preview)
  • Storage Queue Data Contributor (Preview)
  • Storage Queue Data Reader (Preview)

A noter, qu’il est possible de setup ces différentes permissions via la propriété DataAction dans la définition de vos rôles RBAC

Vu que pour le moment le SDK C# pour le Storage ne supporte pas cette nouvelle fonctionnalité il faut le faire via les API REST Azure, comme ci-dessous :

AuthenticationContext authContext = new AuthenticationContext($"https://login.microsoftonline.com/{TenantId}");
AuthenticationResult authResult = await authContext.AcquireTokenAsync($"https://{StorageAccountName}.blob.core.windows.net/", new ClientCredential(ApplicationId, SecretKey));
	
HttpClient client = new HttpClient(); 
client.DefaultRequestHeaders.Add("Authorization", "Bearer " + authResult.AccessToken);
client.DefaultRequestHeaders.Add("x-ms-version", "2017-11-09");

var response = await client.GetStringAsync($"https://{StorageAccountName}.blob.core.windows.net/{ContainerName}/{BlobName}");	

A noter qu’ici, je génère un token grâce à ADAL et que je demande bien celui-ci pour mon compte de stockage.

Avec cet article, vous avez encore moins d’excuses de conserver vos clés de stockage dans vos configs.


SEPT 21

Générer des clés SAS pour vos storage grâce à KeyVault

En entreprise, la sécurité c’est le nerf de la guerre. Il n’est pas envisagable de concevoir une application sans aucune notion de sécurité, que cette application soit hébergée on-premise ou dans le Cloud (Azure de préférence).

Quand vous utilisez des comptes de stockage Azure pour du Blob, des Tables ou des Queues, il est possible que vous souhaitiez ne pas avoir vos clés de stockage dans votre solution ou dans la configuration disponible sur vos serveurs pour des questions de Leakage.

Il est possible de mettre en place plusieurs solutions pour ne pas avoir de clés dans vos configurations :

  • Utiliser un SPN qui aura pour rôle de se connecter à Azure pour récupérer la clé de votre stockage et si besoin générer une SAS Key
  • Mettre en place un SPN qui aura des accès via l’AD sur votre compte de stockage (cela sera traité dans un prochain article)
  • Utiliser un SPN qui aura accès à un KeyVault contenant vos clés de storage
  • Utiliser le KeyVault pour générer des SAS Key.

Cette dernière solution est en preview dans Azure, nous allons voir comment la mettre en place.

La première étape coniste à avoir les droits nécessaires sur le KeyVault pour effectuer l’opération d’ajout, et à attribuer les bons droits aux personnes ou au SPN qui l’utiliseront. A ce jour, il n’est pas possible de faire cette opération via le portail, donc voici un exemple en Powershell :

Set-AzureRmKeyVaultAccessPolicy -VaultName $keyVaultName -ObjectId $userPrincipalId -PermissionsToStorage get,list,delete,set,update,regeneratekey,getsas,listsas,deletesas,setsas,recover,backup,restore,purge

La deuxième étape consiste à associer votre KeyVault à un compte de stockage. Il est possible de faire cela en PowerShell via la commande suivante :

$storage = Get-AzureRMStorageAccount -StorageAccountName $storageAccountName -ResourceGroupName $storageAccountResourgeGroup

Add-AzureKeyVaultManagedStorageAccount -VaultName $keyvaultName -AccountName $storageAccountName -AccountResourceId $storage.Id -ActiveKeyName key2 -DisableAutoRegenerateKey

A noter que sur cette dernière étape il est possible d’activer la régénération des clés de manière automatique sur vos storages.

La dernière étape consiste à construire une SAS Key qui vous servira de template pour la génération des suivantes. Ce template sera par la suite utilisé pour récupérer une nouvelle clé. Voici comment faire cela en powershell :

$sasTokenName = "fullaccess"
$storageKey = (Get-AzureRmStorageAccountKey -ResourceGroupName $storageAccountResourgeGroup -Name $storageAccountName).Value[0]

$context = New-AzureStorageContext -StorageAccountName $storageAccountName -StorageAccountKey $storageKey -Protocol Https
$start = [System.DateTime]::Now.AddMinutes(-15)
$end = [System.DateTime]::Now.AddMinutes(15)
$token = New-AzureStorageAccountSASToken -Service Blob,File,Table,Queue -ResourceType Service,Container,Object -Permission "racwdlup" -Protocol HttpsOnly -StartTime $start -ExpiryTime $end -Context $context

$validityPeriod = $validityPeriod = [System.Timespan]::FromMinutes(30)
Set-AzureKeyVaultManagedStorageSasDefinition -VaultName $keyVaultName -AccountName $storageAccountName -Name $sasTokenName -ValidityPeriod $validityPeriod -SasType 'account' -TemplateUri $token

Il est important d’utiliser un “sasTokenName” explicite et unique pour chacune de vos SAS Key, car c’est celui-là qui sera utilisé par la suite.

Maintenant pour récupérer ma clé SAS, il me suffit d’appeler le KeyVault avec le secret suivant : StorageAccountName-sasTokenName, comme on peut le voir ci-dessous en Powershell :

$sasKey = (Get-AzureKeyVaultSecret -VaultName $keyvaultName -Name ("wwodemospn-fullaccess")).SecretValueText

Avec cette méthode, fini les clés de Storage qui trainent dans les fichiers de config.


SEPT 10

Azure Batch & Windows Container

Récemment j’ai eu la problématique suivante : trouver un moyen de limiter la consommation mémoire d’une tâche dans Azure Batch. S’agissant à la base d’une tâche réalisée en .Net basée sur un framework pas récent, j’ai décidé de conserver au maximum l’applicatif sans pour autant écrire beaucoup de code pour mettre en place cette limitation.

J’ai donc opté pour la mise en place de container Docker pour Windows, puisque Docker permet de mettre cela en place simplement grâce à une option lorsqu’on lance notre container.

En terme de modification, je dois donc juste m’assurer que je puisse packager mon applicatif dans un container docker et que je puisse utiliser l’option sur Batch.

Afin de tester la fonctionnalité Batch + Container Windows, j’ai donc réalisé les tâches suivantes :

  • Création d’un Azure Container Registry
  • Création d’un compte Azure Batch

Ma registry va me servir à créer mon image Docker et à valider l’usage d’une registry privée, et mon compte Azure Batch va me servir pour mes tests.

Commençons par mon code applicatif que je vais utiliser, il doit respecter les critères suivants :

  • Utiliser de la mémoire
  • Tourner avec le Framework .Net (ici je vais prendre le 4.7.2)
  • Avoir une durée de vie assez longue afin de pouvoir observer les différentes métriques et retours lors d’une éxecution

J’ai pris le parti de créer un programme infini qui écrit dans un Table Storage en gardant toutes les entrées en mémoire, cela donne donc ce code :

public static class Program
{
    private const string ConnectionString = "CLE EN DUR, C'EST UN TEST DE HAUT VOL";
    public static void Main(string[] args) 
    {
      var csa = CloudStorageAccount.Parse(ConnectionString);
      var tableClient = csa.CreateCloudTableClient();
      
      var batchTable = tableClient.GetTableReference("batch");
      batchTable.CreateIfNotExists(); 
      batchTable.Execute(TableOperation.Insert(new DynamicTableEntity("batch", (DateTime.MaxValue.Ticks - DateTime.UtcNow.Ticks).ToString()))); 
    	
      List<FakeItem> items = new List<FakeItem>(); 
	
      for (int i = 0; i < int.MaxValue; i++)
      {
        items.Add(new FakeItem()
        {
          Val = i,
          Id = Guid.NewGuid()
        });


        if (i % 100 == 0)
        {
          System.Diagnostics.Process currentProcess = System.Diagnostics.Process.GetCurrentProcess();
          long memoryUsage = currentProcess.WorkingSet64;

          batchTable.Execute(TableOperation.Insert(
          new MyEntity()
          {
            PartitionKey = "batch",
            RowKey = (DateTime.MaxValue.Ticks - DateTime.UtcNow.Ticks).ToString(),
            MemoryValue = memoryUsage
          }));

        }
      }

    }

    public class MyEntity : TableEntity
    {
      public long MemoryValue { get; set;}
    }

    public class FakeItem
    {
      public Guid Id { get; set; }
      public double Val { get; set;}
    }
}

Il ne reste plus qu’à packager tout cela avec un DockerFile, qui est le suivant :

FROM microsoft/dotnet-framework:4.7.2-sdk-windowsservercore-ltsc2016 AS build
WORKDIR /app

COPY *.sln .
COPY sample-app/*.csproj ./sample-app/
RUN dotnet restore

COPY . .
WORKDIR /app/sample-app
RUN dotnet build

FROM build AS publish
WORKDIR /app/sample-app
RUN dotnet publish -c Release -o out


FROM microsoft/dotnet-framework:4.7.2-runtime-windowsservercore-ltsc2016 AS runtime
WORKDIR /app
COPY --from=publish /app/sample-app/out ./

Avec le code et ce dockerfile, je peux soit construire l’image en local et la push sur mon ACR, soit directement en faisant un ACR Build afin de gagner du temps vu que les images Docker pour Windows sont assez volumineuses. Voici la commande pour effectuer un build distant :

az acr build --registry <azureregistryname> --file .\DockerFile --image <appname> --os windows .

Le fonctionnement d’Azure Batch s’articule autour de 3 notions :

  • Pool : Ensemble de serveur qui seront utilisés. Un compte Azure Batch peut contenir plusieurs types de pool
  • Job : Elément logique qui permet de regrouper nos différentes tâches
  • Task : Exécution de nos tâches de calcul, ici il s’agira de l’exécution de notre container

Commençons donc par créer notre pool pour nos tests, il est possible de faire ceci via le portail Azure, ou via du code comme ci-dessous :

ImageReference imageReference = new ImageReference(
		publisher: "MicrosoftWindowsServer",
		offer: "WindowsServer",
		sku: "2016-Datacenter-with-Containers",
		version: "latest");

// Specify a container registry
ContainerRegistry containerRegistry = new ContainerRegistry(
		registryServer: $"{AcrName}.azurecr.io",
		userName: AcrName,
		password: AcrKey);

// Create container configuration, prefetching Docker images from the container registry
ContainerConfiguration containerConfig = new ContainerConfiguration();
	containerConfig.ContainerImageNames = new List<string> {
		// Prefetch images. Unusable node if error
    $"{AcrName}.azurecr.io/{ContainerName}:{ContainerVersion}"
	};
	containerConfig.ContainerRegistries = new[] { containerRegistry };

VirtualMachineConfiguration virtualMachineConfiguration = new VirtualMachineConfiguration(
		imageReference: imageReference,
		nodeAgentSkuId: "batch.node.windows amd64");
	virtualMachineConfiguration.ContainerConfiguration = containerConfig;

// Create pool
CloudPool pool = batchClient.PoolOperations.CreatePool(
  poolId: BatchPoolId,
  virtualMachineSize: "Standard_D2_V2",
  targetDedicatedComputeNodes: 1,
  virtualMachineConfiguration: virtualMachineConfiguration);

pool.UserAccounts = new List<UserAccount>() { new UserAccount("God", "pwdtopsecure", ElevationLevel.Admin) }; 

pool.Commit();

Par ailleurs, pour les tests je ne recommande pas de mettre en place le préchargement des images car en cas d’erreur, Azure Batch indique que votre pool est inutilisable, il faudra donc le recréer, ce qui peut vous faire perdre beaucoup de temps.

Maintenant il faut créer un job et y ajouter une tâche, pour cela encore en CSharp :

string cmdLine = @"C:\app\sample-app.exe";
ContainerRegistry containerRegistry = new ContainerRegistry(
  registryServer: $"{AcrName}.azurecr.io",
  userName: AcrName,
  password: AcrKey);

TaskContainerSettings cmdContainerSettings = new TaskContainerSettings(
  imageName: $"{AcrName}.azurecr.io/{ContainerName}:{ContainerVersion}",
  containerRunOptions: "--memory 64m",
  registry: containerRegistry
  );

CloudTask containerTask = new CloudTask(Guid.NewGuid().ToString("N"), cmdLine);

containerTask.ContainerSettings = cmdContainerSettings;
containerTask.UserIdentity = new UserIdentity(new AutoUserSpecification(elevationLevel: ElevationLevel.Admin, scope: AutoUserScope.Pool));

batchClient.JobOperations.AddTask(job.Id, containerTask);

Le point important à savoir ici c’est pour la ligne de commande qui est exécutée, par défault elle se lance dans le répertoire dédié à votre tâche, et l’application dans votre container se trouve le plus souvent sur le C:

Pour le reste, on peut voir que j’ai la main sur les différentes options pour lancer mon container. Ici je peux donc limiter la mémoire maximale pour mon application. En cas de dépassement de celle-ci, mon code me renvoit une exception de type OutOfMemoryException

Lors de vos tests vous pouvez vous connecter sur les noeuds Batch afin de voir l’utilisation CPU / RAM de vos containers via la commande :

docker stats