Azure API Management est un service Azure sécurisé, fiable et scalable permettant de publier, consommer et gérer des APIs. Cette plateforme permet de renforcer la sécurité et d'analyser l'usage des APIs.

Selon les projets, proposer un front-end pour ses ressources peut répondre à plusieurs besoins:

  1. Gestion de la sécurité directement depuis API Management afin de restreindre l'accès aux ressources à différentes applications ou utilisateurs.
  2. Transformer les accès aux ressources. Aggréger plusieurs ressources en une seule réponse GET par exemple.
  3. Gérer le cache des réponses afin d'alléger les requêtes au Storage.

 

Scénario

Dans cet article, nous allons voir ensemble l'implémentation d'une API permettant de télécharger des Blobs privés depuis Azure Storage. Nous n'aborderons pas la sécurisation de cet API puisque celle-ci pourrait faire l'objet d'un article à part entière.

 

Azure Storage REST API

Nous allons commencer par créer un Compte de Stockage ou Storage Account.

La sécurisation globale de ce compte de stockage est définie dans l'onglet Settings >> Access Keys.

Notez bien comment récupérer la clé Key car elle vous sera utile dans la configuration de votre API.

 

 

Dans de compte nous allons créer un Blob Container. Lors de la création du container, veillez à saisir Public access level  : Private (no anonymous access)

 

Dans ce container, nous allons ajouter un fichier qui sera celui que nous essaierons de télécharger à la fin de cet article.

Pour cela, dans le container, cliquez sur le bouton Upload et laissez vous guider.

 

API Management

Partons du postulat que vous avez déja créé votre ressource API Management puisque celle-ci prend environ 45 minutes à être provisionnée :S

Il vous faut désormais créer une API que l'on appelera "Storage". Lors de la création de votre API, renseignez https://{storageAccountName}.blob.core.windows.net dans la propriété Web Service URL.

Notez également la propriété API URL Suffix, surtout si vous la customisez, puisqu'elle fera partie de la route d'accès à vos ressources.

Ouvrez ensuite votre API, sélectionnez Add Operation et créez une opération avec comme URL GET + /{container}/{blob}.

 

Vous avez désormais une API qui répond aux requêtes entrantes et les redirige vers l'API Blobs d'Azure Storage mais à ce stade, si vous tentez de la consommer vous recevrez une erreur d'authentification.

 

Policy

Nous avons défini une sécurité pour notre container dans Azure Storage (Authentication Type SAS). Nous allons donc désormais indiquer à notre API comment utiliser cette SAS Key pour authentifier les requêtes faites aux ressources.

Sélectionnez votre API puis l'opération créée auparavant et, dans l'onglet Design, sélectionnez le bouton Policies pour passer en mode édition.

La partie inbound correspond aux traitements effectuées sur les requêtes entrantes et c'est ici que nous allons travailler.

Nous allons commencer par définir des variables :

<set-variable name="APIVersion" value="2012-02-12" />
<set-variable name="RequestPath" value="@(context.Request.Url.Path)" />
<set-variable name="UTCNow" value="@(DateTime.UtcNow.ToString("R"))" />

 

Nous allons ensuite ajouter un header date aux requêtes entrantes :

<set-header name="date" exists-action="override">
<value>@(context.Variables.GetValueOrDefault<string>("UTCNow"))</value>
</set-header>
 

Enfin nous ajouterons un header Authorization : 

<set-header name="Authorization" exists-action="override">
<value>@{
var account = "storageproxy";
var key = "QOQDHuUMSB9ztzLn49SUph3LpL2gIjVXpfkpOuy8855kkOdMvlvgz7mJ9S2F6zPgN6nCSPdiy0+AgHQFeetBmQ==";
var splitPath = context.Variables.GetValueOrDefault<string>("RequestPath").Split('/');
var container = splitPath.Reverse().Skip(1).First();
var file = splitPath.Last();
var dateToSign = context.Variables.GetValueOrDefault<string>("UTCNow");
var stringToSign = string.Format("GET\n\n\n{0}\n/{1}/{2}/{3}", dateToSign, account, container, file);
string signature;
var unicodeKey = Convert.FromBase64String(key);
using (var hmacSha256 = new HMACSHA256(unicodeKey))
{
var dataToHmac = Encoding.UTF8.GetBytes(stringToSign);
signature = Convert.ToBase64String(hmacSha256.ComputeHash(dataToHmac));
}
var authorizationHeader = string.Format(
"{0} {1}:{2}",
"SharedKey",
account,
signature);
return authorizationHeader;
}</value>
</set-header>
 

Vous remplacerez bien évidemment les variables account et key par le nom du StorageAccount et la clef SAS définis dans l'étape du Storage.

Les autres lignes du script consistent à récupérer la requêtes entrante, à convertir la SAS Key en signature et à ajouter cette SharedKey composée en tant que header Authorization.

La requête ainsi modifiée est ensuite transmise à l'API de Storage qui en reconnaît l'authentificatione et renvoie la ressource demandée.

 

Test du proxy

Une fois toutes ces étapes suivies, vous devriez vous retrouver avec une API consommable depuis n'importe quel client (navigateur, Postman, etc.) permettant de télécharger vos ressources depuis une url dynamique:

http://{apiManagementUrl}/{apiUrlSuffix}/{container}/{blob}

 

Conclusion

Nous avons vu ensemble comment proxyfier des ressources Azure Storage via une API dans Azure API Management.

Avec cette logique, vous proposez aux utilisateurs ou applications clientes un accès aux ressources agnostique de la plateforme de ressources sous-jacente. Vous pouvez alors désormais gérer votre logique de cache, de connexion aux storages (SPN, MSI ?), apporter de l'intelligence fonctionnelle ou même switcher vers un autre provider sans douleur pour les clients.

 

Aller plus loin

Plusieurs pistes d'évolution et d'amélioration pour votre proxy:

  • Ajouter une authentification type JWT sur vos API pour éviter que ce soit open-bar
  • Appliquer la Policy sur l'API au lieu de l'opération afin qu'elle s'applique sur d'autres opérations
  • Utiliser les NamedValues d'API Management pour stocker vos variables account, key, etc.
  • Utiliser un SPN ou MSI pour éviter de stocker votre SAS Key dans API Management

Cet article traite de la suppression totale ou partielle des entités présentes dans dans une Table Azure Storage.
Nous y aborderons les suppressions complète de table, les batchs et le requêtage par segmentation et les limitations de ces différentes méthodes.

Suppression de la table

Si votre table comporte un nombre importants d'enregistrements, le plus simple est probablement de supprimer la table elle-même et de la recréer.

var storageAccount = CloudStorageAccount.Parse(
    ConfigurationManager.ConnectionStrings["AzureStorage"].ConnectionString);
var tableClient = storageAccount.CreateCloudTableClient();
var rmsTable = tableClient.GetTableReference("ResourceManagerStorage");

rmsTable.DeleteIfExists();

Il est important de noter que l'opération de suppression n'est pas instantanée mais se met dans une file d'éléments à supprimer et que cela peut prendre environ 40 secondes (source MSDN: http://msdn.microsoft.com/en-us/library/windowsazure/dd179387.aspx ).

Si vous tenter de recréer la table durant ce laps de temps vous recevrez une erreur 409 (Conflict) vous indiquant que la table est en train d'être supprimée.

rmsTable.Create();

The remote server returned an error: (409) Conflict.

HTTP/1.1 409 Conflict TableBeingDeleted

The specified table is being deleted.

Try operation later. RequestId:xxxxxxx Time:xxxxxxxx

Le temps de traitement de cette opération dépend de l'utilisation du système. Elle dépend également du nombre d'entrées dans la table.

Le véritable point noir de cette solution est qu'il vous sera donc impossible d'utiliser ou de recréer cette table durant ce laps de temps.

Une piste à creuser est probablement de coder une policy de retry adéquate sur le create  de la table en cas de conflit avant de passer aux solutions suivantes.

Suppression par Batch Transactions

Il se peut toutefois que vous ayez besoin de garder votre table disponible et que le downtime évoqué précédemment ne soit pas envisageable surtout si d'autres applications y accèdent en continu. Dans ce cas là, vous pouvez supprimer les entités en utilisant Entity Batch Transactions.

La documentation concernant les batch transactions est disponible dans la documentation Performing Entity Group Transactions qui explique notamment les détails des API REST utilisées.

Mais là encore nous trouvons des limitations.

En effet les batchs transactions ne sont possibles que sur les entités donc la propriété PartitionKey est identique. Si cela est votre cas, alors pas de problème, sinon, vous devrez supprimer les entités une à une.

Une autre limitation, plus importante est le nombre d'entités qu'un batch peut traiter et il s'agit de 100 entités au maximum.

Requêtage par segmentation

Afin de supprimer un ensemble conséquent de données, nous avons vu qu'il était nécessaire de requêter dans un premier temps les entités concernées. Hors cette étape peut prendre un temps conséquent dépendant notamment du nombre d'entrées dans la table.

Comme le précise la documentation : Il est important de préciser qu'une Table Query est une requête qui retrouve une ensemble d'entités qui ne partagent pas de Partition Key. Ces requêtes ne sont pas performantes et vous devriez les éviter dans la mesure du possible.

Si ce type de requêtage est nécessaire, il convient alors de requêter par segments de manière asynchrone. En réalité, si l'on veut faire les choses proprement avec des méthodes asynchrones, on n'a pas vraiment le choix puisque les méthodes ExecuteAsync et ExecuteQueryAsync n'existent pas.

var query = table.CreateQuery()
    .Where(r => r.MyCustomProperty.Equals("test"))
    .AsTableQuery();
    var continuationToken = default(TableContinuationToken);

    do
    {
      // Execute the query segment
      var result= await query.ExecuteQuerySegmentedAsync(query, continuationToken);
      // Get entities from result
      var entities = result.Results;
      // Delete entities
      if (entities.Any())
      {
      entities.ForEach(async e => await table.ExecuteAsync(TableOperation.Delete(e)));
      }
      // Get the continuation information from results
      continuationToken = results.ContinuationToken;
    } while (continuationToken != null && !ct.IsCancellationTokenRequested);

Notez que si le paramètre TakeCount est supérieur à 1000 alors il sera ignoré et la valeur 1000 sera utilisée par défaut.

Optimisation des requêtes et suppression

Pour utiliser les batchs ou les effectuer des suppressions unitaires il va vous falloir requêter les entités au préalable. N'oubliez pas que pour la suppression, seules les propriétés PartitionKey et RowKey sont nécessaires, alors n'hésitez pas à ne projeter que ces deux propriétés pour optimiser le traitement.

Pour cela, il vous suffit de rajouter un

.Select(new[] {"PartitionKey", "RowKey"})

dans la construction de votre requête pour effectuer la projection et optimiser votre temps de requête.

Dans le paragraphe précédent, vous noterez également que la suppression est réalisée en utilisant la méthode ExecuteAsync asynchrone. Vous pourrez optimiser ces batch de traitements avec un ConfigureAwait(false) si le suivi du contexte de ces suppressions n'est pas important dans votre script.