Explorons les boucles CloudFormation
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 - [A, B, C] - 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 ?