Publié par

Il y a 7 mois -

Temps de lecture 8 minutes

Troposphere ou comment faire du CloudFormation sans douleur

Tous ceux qui ont fait de l’Infrastructure as Code en utilisant le service AWS CloudFormation le savent : ce n’est pas très plaisant à “coder” et difficile à maintenir sur le long terme. En effet, les templates CloudFormation s’écrivent directement en JSON ou YAML, de manière déclarative. De plus, étant donnée la quantité de ressources à créer lors de l’utilisation du moindre service AWS, on atteint rapidement des templates assez volumineux.

Heureusement, il existe d’autres solutions. Nous allons aujourd’hui en voir une nommée Troposhere. Depuis le site officiel, voici sa description :

troposphere – library to create AWS CloudFormation descriptions.

The troposphere library allows for easier creation of the AWS CloudFormation JSON by writing Python code to describe the AWS resources. troposphere also includes some basic support for OpenStack resources via Heat.

To facilitate catching CloudFormation or JSON errors early the library has property and type checking built into the classes.

En résumé, Troposphere est une librairie open source permettant de générer plus facilement des descriptions CloudFormation tout en écrivant du code en Python. Elle contient de plus une gestion des erreurs plus avancée. Tout cela ne promet que de bonnes choses.

Un classique : la déclaration d’une instance EC2

Entrons dans le vif du sujet et voyons comment l’on peut déclarer une instance EC2 en utilisant Troposphere :

from troposphere import Template, ec2

template = Template()

my_ec2 = ec2.Instance("MyEc2Instance")
my_ec2.ImageId = "ami-12345"
my_ec2.InstanceType = "t2.micro"

template.add_resource(my_ec2)

A ce niveau, il n’y a pas vraiment de différence par rapport à du CloudFormation, du moins en terme de longueur de code.

On peut aussi écrire cette déclaration en utilisant un constructeur :

from troposphere import Template, ec2

template = Template()

my_ec2 = ec2.Instance("MyEc2Instance", ImageId="ami-12345", InstanceType="t3.micro")

template.add_resource(my_ec2)

On y gagne un peu mais ce n’est pas assez intéressant pour justifier l’utilisation de la librairie. Le gros avantage de Troposphere est que l’on peut utiliser les fonctionnalités offertes par un langage de programmation tel que Python, comme les méthodes, les conditions et les boucles.

L’avantage d’un vrai langage de programmation

Contrôle d’accès

Si l’on voulait restreindre l’accès en SSH à notre machine EC2 à seulement une dizaine d’IPs différentes, nous le ferions ainsi en CloudFormation :

MySecurityGroup:
  Properties:
    GroupDescription: Enable SSH access to the EC2 machine
    SecurityGroupIngress:
      - CidrIp: 192.168.1.1/32
        FromPort: 22
        IpProtocol: tcp
        ToPort: 22
      - CidrIp: 192.168.1.2/32
        FromPort: 22
        IpProtocol: tcp
        ToPort: 22
      - CidrIp: 192.168.1.3/32
        FromPort: 22
        IpProtocol: tcp
        ToPort: 22

      ......

      - CidrIp: 192.168.1.9/32
        FromPort: 22
        IpProtocol: tcp
        ToPort: 22
      - CidrIp: 192.168.1.10/32
        FromPort: 22
        IpProtocol: tcp
        ToPort: 22
    VpcId: vpc-1234
  Type: AWS::EC2::SecurityGroup

C’est très verbeux, il y a beaucoup de duplication de codes, c’est difficile à maintenir (si l’on veut rajouter une IP, on va vraisemblablement faire un copier coller) et ce n’est vraiment pas plaisant à écrire.

En utilisant une boucle, voici le même exemple avec Troposhere :

from troposphere import Template
from troposphere.ec2 import SecurityGroup

template = Template()

sg_ingress = []
cidrs = ["192.168.1.1/32", "192.168.1.2/32", "...", "192.168.1.9/32", "192.168.1.10/32"]

for cidr_ip in cidrs:
   sg_ingress.append({'FromPort': 22, 'ToPort': 22, 'IpProtocol': 'tcp', 'CidrIp': cidr_ip})

security_group = template.add_resource(SecurityGroup(
   'MySecurityGroup',
   SecurityGroupIngress=sg_ingress,
   VpcId='vpc-1234',
   GroupDescription='Enable SSH access to the EC2 machine'
))

On y gagne en longueur, mais aussi en clarté ainsi qu’en maintenance. Pour rajouter une nouvelle IP autorisée, il suffit de l’ajouter dans le tableau des IPs.

Gestion des types d’instances en fonction de l’environnement

Les besoins entre une plateforme de développement ou de qualification ne sont pas les mêmes que pour la production. Troposphere n’aide pas naturellement à gérer ce type de problèmes, mais le fait d’écrire les templates en Python facilite les choses.

Voyons comment faire grâce à une variable d’environnement.

import os

from troposphere import Template, Ref, Parameter
from troposphere.ec2 import Instance

template = Template()

environment = os.getenv('ENVIRONMENT', 'dev')

def get_instance_type():
   if environment == 'dev':
       return 't2.micro'
   elif environment == 'prod':
       return 'm5.large'

   raise ValueError("Environment {} unknown".format(environment))

t.add_resource(Instance(
   'MyInstance',
   ImageId='ami-951945d0',
   InstanceType=get_instance_type(),
   KeyName='myKey',
   SecurityGroupIds=[Ref(my_security_group)],
   SubnetId='subnet-1234',
))

Le résultat est plutôt simple, bien qu’un peu limité. Dans un projet d’envergure, nos configurations seraient définies dans des fichiers .yaml par exemple, facilement exploitables en Python.

Génération des templates CloudFormation

Pour l’instant, nous avons vu comment écrire des templates grâce à Troposphere en utilisant Python. Mais comment faut-il s’y prendre pour obtenir les templates finaux CloudFormation ?

Troposphere propose une méthode to_XXX() qui indique vers quel langage on désire convertir les templates. Soit YAML ou JSON.

Il ne reste plus qu’à utiliser les mécanismes Python pour afficher le résultat sur la sortie standard ou écrire le résultat dans un fichier :

# sortie standard
print(template.to_yaml())
print(template.to_json())

# écriture dans un fichier
with open('exemple.yaml', 'w') as f:
    f.write(t.to_yaml())

with open('exemple.json', 'w') as f:
    f.write(t.to_json())

La génération du template se fait ensuite simplement par l’exécution de la commande suivante : python template.py

Pourquoi avons-nous besoin de générer les templates CloudFormation alors que nous faisons tout avec Troposhere ? Malheureusement, Troposhere ne propose aucune solution pour ensuite déployer les ressources ainsi créées sur AWS. Il nous faudra continuer à utiliser les outils habituels (AWS CLI, Brume, etc…). De ce fait, Troposphere n’agit pas comme une surcouche à la pile d’outils habituelle que l’on utilise avec AWS mais comme un outil à utiliser à côté, sans dépendance forte. Il est très facile de s’en séparer si on le souhaite.

Gestion des erreurs

La seule validation qu’effectue CloudFormation lors du déploiement d’une pile est que le template soit valide structurellement (YAML ou JSON valide). C’est un peu léger.

Troposhere apporte trois validations supplémentaires :

  • Typage

TypeError: <class 'troposphere.ec2.Instance'>: MyInstance.ImageId is <class 'int'>, expected <class 'str'>

  • Champ requis

ValueError: Resource GroupDescription required in type AWS::EC2::SecurityGroup (title: MySecurityGroup)

  • Champ inconnu

AttributeError: AWS::EC2::SecurityGroup object does not support attribute GroupDescripon

C’est un énorme bénéfice qui vous évitera de nombreux allers-retours dans la console CloudFormation.

Tests automatisés

Le fait d’utiliser Python nous apporte la possibilité de tester facilement nos templates. Prenons en exemple la création d’un bucket S3 :

from troposphere import Output, Ref, Template
from troposphere.s3 import Bucket

template = Template()

def create_bucket(template):
   return template.add_resource(Bucket("TestBucket",
                                       BucketName="my-test-bucket"))

def add_bucket_output(template, s3_bucket):
   return template.add_output(Output(
       "BucketName",
       Value=Ref(s3_bucket),
       Description="Name of S3 bucket"
   ))

if __name__ == "__main__":
   s3_bucket = create_bucket(template)
   s3_output = add_bucket_output(template, s3_bucket)

On peut rajouter deux tests unitaires qui vont vérifier que le bucket est bien créé avec les paramètres attendus, ainsi qu’un output est bien généré.

import unittest

from troposphere import Ref, Template
from troposphere.s3 import Bucket
from s3_bucket import create_bucket, add_bucket_output

class BucketTest(unittest.TestCase):

   def test_should_create_s3_bucket(self):
       # GIVEN
       template = Template()

       # WHEN
       s3_bucket = create_bucket(template)

       # THEN
       self.assertEqual(s3_bucket.title, "TestBucket")
       self.assertEqual(s3_bucket.BucketName, "my-test-bucket")

   def test_should_add_output_for_bucket(self):
       # GIVEN
       template = Template()
       s3_bucket = Bucket("TestBucket",
                          BucketName="my-test-bucket")

       # WHEN
       output = add_bucket_output(template, s3_bucket)

       # THEN
       self.assertEqual(output.title, "BucketName")
       self.assertEqual(output.Value, Ref(s3_bucket))
       self.assertEqual(output.Description, "Name of S3 bucket")

if __name__ == '__main__':
   unittest.main()

Tester la génération de son infrastructure avant de l’exécuter sur AWS est encore une fois un gros gain de temps.

Une documentation en retrait

Troposphere est une librairie qui existe depuis 2013, très souvent mise à jour pour faire suite aux évolutions ou aux nouveaux services AWS. Cependant elle n’est pas très bien documentée, la meilleure documentation restant les exemples fournis dans le repository GitHub ainsi que la documentation de CloudFormation.

Par contre, le code source de la librairie est une documentation en lui-même. On peut ainsi voir les spécifications de chaque type de ressources. Voici un exemple pour la ressource SecurityGroup :

class SecurityGroup(AWSObject):
   resource_type = "AWS::EC2::SecurityGroup"

   props = {
       'GroupName': (str, False),
       'GroupDescription': (str, True),
       'SecurityGroupEgress': (list, False),
       'SecurityGroupIngress': (list, False),
       'VpcId': (str, False),
       'Tags': ((Tags, list), False),
   }

On y voit la liste des paramètres attendus, la façon de les écrire, s’ils sont requis ainsi que leurs types. C’est plutôt utile et évite de nombreux allers-retours avec la documentation de CloudFormation.

Conclusion

En conclusion, Troposphere répond parfaitement au besoin et fait exactement ce qu’on lui demande. On fait de l’infra as code en écrivant du vrai code. La librairie n’est pas trop intrusive, fait gagner en lignes de code (sur un gros projet, environ 50% de lignes en moins qu’en pur CloudFormation) et surtout, pour peu que l’on connaisse un minimum le Python, c’est beaucoup plus plaisant à coder que du YAML ou JSON.

Publié par

Commentaire

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Nous recrutons

Être un Sapient, c'est faire partie d'un groupe de passionnés ; C'est l'opportunité de travailler et de partager avec des pairs parmi les plus talentueux.