Explorons les boucles CloudFormation

Temps de lecture : 7 minutes

Le 26 juillet, AWS a annoncé que AWS CloudFormation proposait désormais une syntaxe de boucle. Je suis très enthousiaste à propos de cette annonce car pendant plus de 6 ans maintenant, j’ai considéré l’absence de « boucles » comme l’un des principaux inconvénients de CloudFormation par rapport à Terraform (avec son incapacité à gérer une stack multi-comptes/multi-régions). Bien sûr, soyons honnêtes, CloudFormation présente également d’énormes avantages, comme sa simplicité et sa robustesse bien supérieure à celle de Terraform grâce à sa capacité à effectuer automatiquement un rollback en cas d’échec.

Quoi qu’il en soit, aujourd’hui, je souhaite partager mon expérience de refactoring d’un template VPC simple en utilisant la nouvelle syntaxe Fn::ForEach !

Un simple VPC

Les templates que je présente ici créent un simple VPC IPv4 avec 2 niveaux de subnets (public et privé) dans 3 zones de disponibilité (donc, 6 subnets au total):

J’utilise des « NAT instances » car souvent j’ai juste besoin d’un VPC à des fins de test et payer pour des « NAT Gateway » me semble être un gaspillage d’argent quand tout ce dont j’ai besoin c’est fournir un accès Internet à quelques petites instances qui vont simplement récupérer des paquets Linux.

Enfin et comme toujours, j’ajoute les deux Gateway VPC Endpoints pour Amazon S3 et Amazon DynamoDB, peu importe que j’en aie besoin ou non, car ils sont complètement gratuits et ne peuvent donc pas avoir un impact négatif sur la configuration. Dans le meilleur des cas, ils me feront économiser de l’argent et/ou offriront de meilleures performances.

Un coup d’oeil au template « classique »

Le template CloudFormation « classique » pour créer un VPC de ce type peut être trouvé sur mon GitHub: template classique. Je vous encourage à y jeter un œil pour bien voir l’ampleur des répétitions.

Sans rentrer trop dans les détails, voici un condensé des ressources présentes dans le template:

Resources:
  VPC:
    Type: AWS::EC2::VPC
  PublicSubnetA:
    Type: AWS::EC2::Subnet
  PublicSubnetRouteTableAssociationA:
    Type: AWS::EC2::SubnetRouteTableAssociation
  PrivateSubnetA:
    Type: AWS::EC2::Subnet
  NATInstanceA:
    Type: AWS::EC2::Instance
  PrivateRouteTableA:
    Type: AWS::EC2::RouteTable
  PrivateRouteA:
    Type: AWS::EC2::Route
  PrivateSubnetRouteTableAssociationA:
    Type: AWS::EC2::SubnetRouteTableAssociation
  PublicSubnetB:
    Type: AWS::EC2::Subnet
  PublicSubnetRouteTableAssociationB:
    Type: AWS::EC2::SubnetRouteTableAssociation
  PrivateSubnetB:
    Type: AWS::EC2::Subnet
  NATInstanceB:
    Type: AWS::EC2::Instance
  PrivateRouteTableB:
    Type: AWS::EC2::RouteTable
  PrivateRouteB:
    Type: AWS::EC2::Route
  PrivateSubnetRouteTableAssociationB:
    Type: AWS::EC2::SubnetRouteTableAssociation
  PublicSubnetC:
    Type: AWS::EC2::Subnet
  PublicSubnetRouteTableAssociationC:
    Type: AWS::EC2::SubnetRouteTableAssociation
  PrivateSubnetC:
    Type: AWS::EC2::Subnet
  NATInstanceC:
    Type: AWS::EC2::Instance
  PrivateRouteTableC:
    Type: AWS::EC2::RouteTable
  PrivateRouteC:
    Type: AWS::EC2::Route
  PrivateSubnetRouteTableAssociationC:
    Type: AWS::EC2::SubnetRouteTableAssociation

Vous voyez où je veux en venir là ? Toutes ces ressources suffixées par A, B ou C sont en réalité exactement les mêmes, mais créées pour chaque zone de disponibilité. Si vous regardez le template complet, vous verrez que les valeurs des propriétés de ces ressources varient essentiellement d’une lettre. Cela semble être un bon cas d’utilisation pour faire une boucle !

Divisons le par deux avec des boucles !

Comme précédemment, le template CloudFormation « loop » qui crée le même VPC en utilisant des boucles peut être trouvé sur mon GitHub : loop template. Et bien sûr, vous pouvez l’utiliser comme vous le souhaitez !

La nouvelle fonctionnalité de boucles de CloudFormation est introduite par une nouvelle fonction intrinsèque : Fn::ForEach. Pour utiliser cette nouvelle fonction, vous devez indiquer à CloudFormation d’utiliser une transformation en ajoutant la ligne suivante au début de votre modèle :

Transform: AWS::LanguageExtensions

Là, vous avez un premier indice sur ce qui va réellement se passer : le moteur d’interprétation CloudFormation n’a pas vraiment changé, il est simplement capable d’appliquer un prétraitement à votre template et de le modifier. De fait, vous devrez autoriser explicitement CloudFormation à le faire en fournissant la capacité CAPABILITY_AUTO_EXPAND lors de la création de la stack, lui indiquant ainsi que vous comprenez que le modèle que vous fournissez ne sera pas le véritable modèle utilisé pour créer la stack.

La modification de mon premier template pour utiliser des boucles est en fait assez simple. Nous avons déjà remarqué que les ressources sont répétées avec seulement une lettre qui change d’une zone de disponibilité à une autre. C’est donc le point de départ évident : faire une boucle sur les valeurs A, B et C pour cette lettre.

Sans entrer dans trop de détails encore une fois, voici le nouveau condensé des ressources présentes dans le template :

Resources:
  VPC:
    Type: AWS::EC2::VPC
  Fn::ForEach::AZ:
  - az
  - [ABC]
  - PublicSubnet${az}:
      Type: AWS::EC2::Subnet
    PublicSubnetRouteTableAssociation${az}:
      Type: AWS::EC2::SubnetRouteTableAssociation
    PrivateSubnet${az}:
      Type: AWS::EC2::Subnet
    NATInstance${az}:
      Type: AWS::EC2::Instance
    PrivateRouteTable${az}:
      Type: AWS::EC2::RouteTable
    PrivateRoute${az}:
      Type: AWS::EC2::Route
    PrivateSubnetRouteTableAssociation${az}:
      Type: AWS::EC2::SubnetRouteTableAssociation

Et grâce à la magie de Fn::ForEach, CloudFormation prétraite le template et le transforme pour obtenir exactement la même chose que précédemment.

Pour utiliser Fn::ForEach, vous devez lui donner un nom unique, ici j’utilise Fn::ForEach::AZ.

Comme vous pouvez le voir, Fn::ForEach attend 3 arguments :

  • Le nom de la variable d’itération, ici j’utilise az, mais c’est arbitraire ;
  • Une liste de strings sur laquelle ForEach va itérer ;
  • Le “truc” qui doit être répliqué avec cette boucle ForEach.

Vous pouvez utiliser Fn::ForEach dans les Conditions, les Resources, les propriétés de ressources et les Outputs de vos templates.

Si vous lisez mon template complet, vous repérerez quelques astuces supplémentaires que j’utilise, comme obtenir ma liste de strings à partir d’un mapping ou utiliser un autre mapping pour convertir les lettres en entiers (voir plus loin). Il y a aussi un exemple d’une double boucle ForEach imbriquée dans ma section Outputs. C’est intuitif vous verrez 😉

Limitations

Pas de boucles dynamiques à l’exécution

Gardez à l’esprit que vos boucles seront étendues avant que CloudFormation ne commence réellement à traiter votre template et à créer des ressources. Par conséquent, il est impossible d’utiliser les valeurs de retour d’une ressource provisionnée comme une collection pour itérer. Par exemple, !GetAtt VPC.Ipv6CidrBlocks est bien une collection de strings, mais elle ne peut pas être connue avant la création réelle du VPC, vous ne pouvez donc pas l’utiliser comme un argument pour Fn::ForEach.

Fonctionne seulement avec des listes

Une de mes déceptions a été de découvrir que seules les listes de strings sont acceptées comme collection à parcourir, et donc la variable d’itération est toujours une simple chaîne de caractères.

J’avoue que j’espérais quelque chose de plus avancé, comme des listes d’objets, avec la variable d’itération étant un objet. Je pense que ça aurait été une fonctionnalité beaucoup plus puissante sans la rendre fondamentalement plus compliquée à implémenter ou à comprendre.

Dans mon exemple simple de VPC, nous pouvons voir qu’en ayant une seule variable itérant sur la liste [A, B, C], je suis obligé de tricher un peu pour mapper les lettres en indices que je peux utiliser pour sélectionner la bonne zone de disponibilité (AZ) ou le bon CIDR de subnet. J’aurais pu utiliser une autre solution de contournement, comme itérer sur ['0', '1', '2'] à la place (ce qui fonctionne avec !Select), mais cela aurait signifié changer ma convention de nommage pour utiliser des nombres au lieu de lettres et surtout accepter un index partant de zéro dans des noms destinés à être lus par des humains… Pas du tout satisfaisant.

Avec un itérateur de type “objet” sur une liste d’objets, nous pourrions utiliser !GetAtt object.property ou la syntaxe ${object.property} ce qui aurait été bien plus pratique à mon avis, mais bon ! Peut-être qu’un jour, n’est-ce pas ? En attendant, je suppose que l’utilisation de Mappings comme solution de contournement, comme je l’ai fait, est acceptable.

Plante aws cloudformation package

Ca, c’est vraiment ballot. Au moment d’écrire cet article, l’utilisation de Fn::ForEach dans votre template casse complètement la commande aws cloudformation package. La commande plante et malheureusement je n’ai trouvé aucun contournement. Donc vous ne pouvez probablement pas utiliser cette nouvelle fonctionnalité dans vos pipelines CICD pour l’instant, sauf si vous n’utilisez pas du tout aws cloudformation package, mais ça semble peu probable.

J’espère qu’AWS corrigera ça très rapidement et j’ai créé une issue GitHub qui sera probablement mise à jour pour suivre la résolution : https://github.com/aws/aws-cli/issues/8075.

Conclusion

Malgré certaines limitations décevantes, c’est quand même une nouvelle fonctionnalité bienvenue. Ce que j’apprécie, c’est qu’elle est très simple : il n’est pas nécessaire de changer nos habitudes car le nouveau ForEach s’intègre intuitivement dans notre boîte à outils. Cela ne crée pas trop de confusion et nos template CloudFormation restent très lisibles (à condition d’utiliser YAML bien sûr). Je suis prêt à parier que quelqu’un qui tombe sur cette fonctionnalité en lisant un template comprendra intuitivement ce qu’elle fait.

Dans l’ensemble, j’avais presque 400 lignes de code pour décrire mon VPC et Fn::ForEach m’a permis d’obtenir exactement le même résultat en moins de 250 lignes, ce qui est quand même cool 🙂

Commentaires :

A lire également sur le sujet :