AWS Certified CloudOps Engineer - Associate

Fondamentaux de CloudFormation

Comment un template CloudFormation décrit une infrastructure : les sections du template, des parameters avec de vrais garde-fous, les fonctions intrinsèques, le graphe de dépendances que CloudFormation construit à votre place, et les attributs de ressource qui décident de ce qui survit à une suppression.

Intermédiaire 30 minutes 7 Objectifs d'apprentissage
  1. Distinguer un template, une stack, un logical ID et un physical ID
  2. Identifier les sections d'un template et expliquer laquelle est obligatoire
  3. Choisir le type de parameter et les contraintes qui bloquent une mauvaise saisie avant toute création de ressource
  4. Sélectionner la bonne fonction intrinsèque pour une référence donnée, et expliquer pourquoi Ref et Fn::GetAtt ne sont pas interchangeables
  5. Prédire l'ordre dans lequel CloudFormation crée les ressources, et savoir quand DependsOn est nécessaire
  6. Comparer Fn::ImportValue, Fn::GetStackOutput et les outputs de nested stacks pour partager une valeur entre stacks
  7. Garder les identifiants hors d'un template avec les dynamic references, et énoncer ce que NoEcho ne protège pas

Une équipe a construit son environnement de staging à la main : un VPC, deux subnets, un security group, un load balancer, un Auto Scaling group et une instance RDS. Un après-midi de clics. La production a maintenant besoin de la même chose, dans une seconde Region, et personne ne sait reconstituer de mémoire les règles exactes du security group. Quelqu'un ouvre la console à côté de celle de staging et commence à recopier.

C'est ce problème que CloudFormation supprime. Vous décrivez l'infrastructure une fois, dans un fichier, et AWS la construit. Reconstruisez-la la semaine prochaine dans un autre compte et vous obtenez le même résultat, parce que la description fait référence à la place de la mémoire de quelqu'un.

Template, stack, et les deux sortes d'identifiant

Trois mots portent l'essentiel du sens, et les confondre rend tous les concepts suivants plus difficiles.

Un template est le fichier. JSON ou YAML, stocké dans Git, relu comme du code, et sans effet par lui-même.

Une stack est ce que vous obtenez quand CloudFormation traite ce template : un ensemble vivant de ressources AWS gérées comme une seule unité. Mettez la stack à jour et CloudFormation détermine quelles ressources doivent changer. Supprimez la stack et, par défaut, tout ce qu'elle contient disparaît.

Dans le template, chaque ressource porte un logical ID, le nom que vous choisissez :

Resources:
  WebSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Allow HTTPS from the load balancer
      VpcId: !Ref AppVpc

WebSecurityGroup est le logical ID. Il n'existe que dans ce template et cette stack. Quand CloudFormation crée le groupe, AWS lui attribue un physical ID du genre sg-0a1b2c3d4e5f. Le logical ID sert à désigner la ressource pendant l'écriture ; le physical ID est ce qui existe réellement dans le compte.

Cette distinction devient porteuse plus loin. Renommez un logical ID et CloudFormation ne voit pas un renommage, il voit une ressource supprimée et une autre créée. Le physical ID est aussi ce qui change quand une ressource est remplacée pendant une mise à jour, ce qui fait toute la différence entre une mise à jour anodine et une interruption de service.

Un dernier cadrage qui sert dans tout le sujet : un template est déclaratif. Vous énoncez l'état final, pas les étapes. Vous n'écrivez jamais « crée le VPC, attends, puis crée le subnet ». Vous déclarez que le subnet appartient au VPC, et CloudFormation en déduit l'ordre.

Les sections d'un template

Un template compte dix sections possibles de premier niveau. Une seule est obligatoire.

SectionRôle
ResourcesObligatoire. Les ressources AWS à créer, chacune avec un logical ID, un Type et des Properties
ParametersDes valeurs fournies à la création ou à la mise à jour, pour qu'un template serve plusieurs environnements
MappingsUne table de correspondance statique lue avec Fn::FindInMap, souvent indexée par Region ou par environnement
ConditionsDes expressions booléennes nommées qui décident si une ressource ou une propriété est incluse
OutputsDes valeurs renvoyées après construction de la stack, exportables vers d'autres stacks
TransformLes macros à exécuter sur le template, dont AWS::Serverless (SAM) et AWS::LanguageExtensions
MetadataDes données libres sur le template, y compris des indications d'interface pour la console
RulesValide un parameter ou une combinaison de parameters avant le provisionnement
AWSTemplateFormatVersionLa version du format de template, 2010-09-09
DescriptionUne description textuelle du template

Un template qui ne déclare que Resources est valide. Tout le reste gagne sa place en rendant le template réutilisable, plus sûr ou plus lisible.

L'ordre des sections dans le fichier est indifférent à CloudFormation, et l'ordre des ressources dans Resources aussi. Cela surprend ceux qui attendent un script.

Parameters : des entrées avec garde-fous

Un template écrit en dur est un template utilisable une seule fois. Les parameters sont la façon dont un même fichier sert dev, staging et production.

Parameters:
  EnvironmentName:
    Type: String
    AllowedValues: [dev, staging, prod]
    Description: Which environment this stack represents

  AppVpcId:
    Type: AWS::EC2::VPC::Id
    Description: The VPC to deploy into

  LatestAmiId:
    Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
    Default: /aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2

Trois types de parameter différents y font trois métiers différents, et l'écart compte davantage que ne le laisse penser la syntaxe.

EnvironmentName est un String verrouillé par AllowedValues. Les contraintes (AllowedValues, AllowedPattern, MinLength, MaxLength, MinValue, MaxValue) sont vérifiées avant qu'aucune ressource ne soit touchée, donc une faute de frappe échoue en quelques secondes au lieu d'échouer à mi-chemin d'un déploiement de 12 minutes. Ajoutez ConstraintDescription et le message d'erreur dit « must be dev, staging, or prod » au lieu de jeter une expression régulière à la figure de l'utilisateur.

AppVpcId utilise un type de parameter propre à AWS. CloudFormation vérifie que le VPC existe bien dans ce compte et cette Region, et la console affiche une liste déroulante plutôt qu'un champ libre. Il existe des types pour les availability zones, les ID d'AMI, les instances, les key pairs, les security groups, les subnets, les volumes, les VPC et les hosted zones Route 53, plus une forme List<> de chacun.

LatestAmiId utilise un type de parameter SSM. Vous fournissez une clé Parameter Store, et CloudFormation en récupère la valeur courante à la création ou à la mise à jour. Cette ligne unique remplace toute la table Region vers AMI que trimballaient les anciens templates, et c'est la raison d'être des parameters publics AWS /aws/service/ami-amazon-linux-latest/.... Attention au sens de « courante » ici : la valeur est résolue au moment où l'opération s'exécute, donc une stack créée le mois dernier garde l'ID d'AMI du mois dernier jusqu'à sa prochaine mise à jour.

Les types de base sont String, Number, List<Number> et CommaDelimitedList. Un template peut déclarer jusqu'à 200 parameters.

La fausse idée à tuer tout de suite : les parameters ne sont pas un mécanisme de gestion des secrets. NoEcho: true masque un parameter par des astérisques dans describe-stacks et describe-stack-events, ce qui est utile, mais c'est un filtre d'affichage et rien d'autre. Il ne masque pas la valeur dans la section Outputs, ni dans la section Metadata, ni dans l'attribut Metadata d'une ressource, et la valeur transite quand même par votre appel d'API et vos logs de CI. Les secrets appartiennent à une dynamic reference, vue plus bas dans cette leçon.

Les fonctions intrinsèques

Un template doit désigner des choses qui n'existent pas encore. L'instance a besoin de l'ID du security group, mais le security group n'a pas d'ID tant que CloudFormation ne l'a pas créé. Les fonctions intrinsèques sont la façon d'écrire « quelle que soit la valeur que cela prendra ».

Ref renvoie l'identifiant principal d'une ressource, ou la valeur d'un parameter.

SubnetId: !Ref PrivateSubnetA     # renvoie subnet-0abc123
InstanceType: !Ref InstanceType   # renvoie la valeur du parameter

Ce que renvoie Ref dépend du type de ressource et se lit dans sa documentation. Pour AWS::EC2::Instance c'est l'ID de l'instance. Pour AWS::S3::Bucket c'est le nom du bucket. Pour AWS::IAM::Role c'est le nom du rôle, pas l'ARN, et c'est une panne d'une ligne très fréquente.

Fn::GetAtt renvoie un attribut nommé plutôt que l'identifiant.

RoleArn: !GetAtt AppRole.Arn
DnsName: !GetAtt AppLoadBalancer.DNSName

Lisez la frontière une bonne fois et vous cessez de deviner : Ref vous donne l'unique identifiant que le type de ressource désigne, GetAtt vous donne n'importe quel autre attribut publié, par son nom. Quand un énoncé demande un ARN et que le Ref du type renvoie un nom, la réponse est GetAtt.

Fn::Sub fait de la substitution de chaîne, et c'est le remplaçant lisible des Fn::Join imbriqués.

BucketName: !Sub "${EnvironmentName}-app-logs-${AWS::AccountId}"
Arn: !Sub "arn:${AWS::Partition}:s3:::${LogBucket}/*"

Ces valeurs ${AWS::...} sont des pseudo parameters : des valeurs que CloudFormation fournit sans que vous les déclariez. AWS::Region, AWS::AccountId, AWS::StackName, AWS::StackId, AWS::Partition, AWS::URLSuffix et AWS::NoValue. Les employer à la place de valeurs codées en dur est ce qui rend un template portable entre comptes, Regions et partitions comme GovCloud.

Le reste de l'outillage courant, brièvement :

FonctionÀ utiliser pour
Fn::FindInMapLire une valeur dans la section Mappings
Fn::ImportValueLire une valeur exportée par une autre stack, même compte et même Region
Fn::GetStackOutputLire n'importe quel output de stack, y compris cross-Region et cross-account
Fn::Join / Fn::SplitConstruire une chaîne à partir de morceaux, ou la découper
Fn::SelectChoisir un élément d'une liste par son index
Fn::GetAZsObtenir les availability zones d'une Region sous forme de liste
Fn::Base64Encoder du user data
Fn::CidrDécouper des blocs CIDR de subnet dans le CIDR d'un VPC
Fn::If, Fn::Equals, Fn::Not, Fn::And, Fn::OrLa logique des conditions
Fn::ForEach, Fn::Length, Fn::ToJsonStringBoucles et utilitaires, uniquement avec le transform AWS::LanguageExtensions

Les fonctions intrinsèques ne s'utilisent pas partout. Elles sont autorisées dans les propriétés de ressources, les outputs, les attributs metadata, les attributs d'update policy et les conditions. Elles sont interdites dans la section Parameters, ce qui explique qu'une valeur par défaut de parameter ne puisse jamais être calculée.

Le graphe de dépendances que vous n'avez pas eu à écrire

C'est ici que « déclaratif » cesse d'être un mot abstrait.

Resources:
  AppVpc:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16

  WebSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Web tier
      VpcId: !Ref AppVpc

  WebInstance:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: !Ref LatestAmiId
      SecurityGroupIds:
        - !Ref WebSecurityGroup

Rien dans ce template n'énonce d'ordre. Mais WebSecurityGroup renvoie à AppVpc, et WebInstance renvoie à WebSecurityGroup, donc CloudFormation construit un graphe orienté à partir de ces références et crée les ressources dans un ordre qui le satisfait : VPC, puis security group, puis instance. Les ressources sans arête entre elles sont créées en parallèle, ce qui explique qu'une stack de 40 ressources ne prenne pas 40 fois plus de temps qu'une stack d'une ressource.

La suppression parcourt le même graphe à l'envers. C'est pour cela que vous ne pouvez pas supprimer une stack de VPC tant qu'une instance issue de ce VPC existe, et qu'un ordre de suppression inattendu trahit presque toujours une référence inattendue.

Il arrive que deux ressources doivent être ordonnées sans qu'aucune ne renvoie à l'autre. Le cas classique est une Elastic IP dans un VPC : elle exige que l'internet gateway soit attaché avant d'être allouée, mais rien dans ses propriétés ne mentionne l'attachement. Pour cela, et seulement pour cela, vous déclarez l'arête vous-même :

  NatEip:
    Type: AWS::EC2::EIP
    DependsOn: AttachGateway
    Properties:
      Domain: vpc

Sortez DependsOn quand une référence ne peut pas exprimer l'ordre, jamais comme précaution générale. En saupoudrer partout sérialise une stack qui aurait pu se déployer en parallèle, et masque les vraies relations au prochain lecteur.

Conditions et Mappings : un template, plusieurs environnements

La production a besoin d'une base Multi-AZ et d'un hôte bastion. Le dev n'a besoin ni de l'un ni de l'autre, et payer les deux serait absurde. Deux mécanismes traitent cela sans dupliquer le template.

Les Mappings sont une table de correspondance statique :

Mappings:
  EnvConfig:
    dev:
      InstanceType: t3.small
      MinSize: 1
    prod:
      InstanceType: m6i.large
      MinSize: 3

Resources:
  AppGroup:
    Type: AWS::AutoScaling::AutoScalingGroup
    Properties:
      MinSize: !FindInMap [EnvConfig, !Ref EnvironmentName, MinSize]

Les Conditions décident si quelque chose existe :

Conditions:
  IsProduction: !Equals [!Ref EnvironmentName, prod]

Resources:
  BastionHost:
    Type: AWS::EC2::Instance
    Condition: IsProduction
    Properties:
      # ...

  AppDatabase:
    Type: AWS::RDS::DBInstance
    Properties:
      MultiAZ: !If [IsProduction, true, false]
      DBSnapshotIdentifier: !If [IsProduction, !Ref SnapshotId, !Ref "AWS::NoValue"]

Deux détails de ce dernier bloc méritent qu'on s'arrête. Une Condition posée sur une ressource contrôle sa création ; un Fn::If à l'intérieur d'une propriété contrôle la valeur de cette propriété. Et AWS::NoValue est le pseudo parameter qui signifie « omets complètement cette propriété », seul moyen d'exprimer conditionnellement l'absence d'une propriété facultative.

La règle de décision : les mappings pour les valeurs qui diffèrent, les conditions pour les ressources et les propriétés qui existent ou non.

Outputs, et le partage de valeurs entre stacks

Une stack qui construit un VPC ne sert pas à grand-chose si rien d'autre ne peut retrouver les ID de subnets.

Outputs:
  AppVpcId:
    Description: VPC created by this stack
    Value: !Ref AppVpc
    Export:
      Name: !Sub "${AWS::StackName}-VpcId"

Value rend l'output visible sur la stack. Ajouter Export le publie sous un nom qui doit être unique par compte et par Region, ce qui explique l'habitude standard de le préfixer avec ${AWS::StackName}.

Une autre stack le consomme ensuite :

  AppSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !ImportValue network-stack-VpcId

Il existe donc trois façons de passer une valeur d'une stack à une autre, et elles diffèrent par leur force et leur portée, pas par leur syntaxe.

Fn::ImportValueFn::GetStackOutputOutputs de nested stack
Exige un Export côté producteurOuiNonNon
Même compte, même RegionOuiOuiOui
Cross-RegionNonOui, avec le paramètre RegionNon
Cross-accountNonOui, avec un RoleArnNon
Force de la référenceForte : le producteur ne peut pas être supprimé tant que la valeur est importéeFaible : résolue au déploiement seulementDétenue par la stack parente
La valeur exportée peut-elle changer en cours d'usage ?Non, l'export est verrouilléOui, silencieusementGérée avec le parent

Cette colonne de force est tout l'arbitrage. Fn::ImportValue vous donne l'intégrité référentielle : CloudFormation refuse de supprimer la stack exportatrice, et refuse de modifier ou retirer un export importé ailleurs. Cette protection est réellement précieuse et réellement pénible, parce qu'elle interdit de renommer l'export d'un subnet sans d'abord débrancher tous les consommateurs. Fn::GetStackOutput abandonne la protection pour acheter la portée cross-Region et cross-account, et résout la valeur à neuf à chaque opération.

Garder les identifiants hors du fichier

Ne mettez jamais un mot de passe dans un template. Facile à approuver, facile à violer par inadvertance, donc utilisez le mécanisme prévu : les dynamic references.

  AppDatabase:
    Type: AWS::RDS::DBInstance
    Properties:
      MasterUsername: '{{resolve:ssm:/app/db/username}}'
      MasterUserPassword: '{{resolve:secretsmanager:prod/app/db:SecretString:password}}'

Trois motifs existent :

  • {{resolve:ssm:parameter-name:version}} pour une valeur Parameter Store en clair.
  • {{resolve:ssm-secure:parameter-name:version}} pour un parameter SecureString.
  • {{resolve:secretsmanager:secret-id:SecretString:json-key:version-stage:version-id}} pour un secret Secrets Manager.

CloudFormation résout la référence au moment où il a besoin de la valeur et ne la stocke jamais. Faites tourner le secret et l'opération de stack suivante prend la nouvelle valeur sans modifier le template.

Les limites sont petites mais réelles : un template accepte jusqu'à 60 dynamic references, elles ne sont pas prises en charge dans les metadata AWS::CloudFormation::Init ni dans le UserData EC2, et elles ne sont pas résolues avant l'exécution d'un transform. Une référence qui se termine par une barre oblique inverse ne se résout pas du tout.

Les attributs de ressource qui changent le rayon d'impact

Les attributs de ressource se placent à côté de Type et Properties, et modifient ce que CloudFormation fait à la ressource plutôt que ce qu'est la ressource.

DeletionPolicy décide de ce qui arrive quand la ressource quitte la stack.

ValeurEffet
DeleteSupprimer la ressource. La valeur par défaut pour presque tous les types.
RetainConserver la ressource et la sortir du périmètre de CloudFormation. Elle reste facturée.
RetainExceptOnCreateSe comporte comme Retain, sauf qu'une ressource créée par une opération qui part ensuite en rollback est supprimée.
SnapshotPrendre un snapshot, puis supprimer. Pris en charge sur les volumes EBS, les clusters et instances RDS, DocumentDB et Neptune, les clusters et replication groups ElastiCache, et les clusters Redshift.

L'exception qui piège : AWS::RDS::DBCluster, et toute AWS::RDS::DBInstance sans DBClusterIdentifier, ont Snapshot par défaut, pas Delete. Supprimez une telle stack et vous héritez d'un snapshot survivant et d'une facture survivante.

UpdateReplacePolicy fait le même travail pour un autre événement. DeletionPolicy couvre le retrait d'une ressource de la stack ; UpdateReplacePolicy couvre son remplacement pendant une mise à jour, où CloudFormation crée une nouvelle ressource physique et supprime l'ancienne. Poser DeletionPolicy: Retain sur une base et oublier UpdateReplacePolicy: Retain vous protège contre une suppression de stack et vous laisse nu face au remplacement accidentel, bien plus probable. Posez les deux.

  AppDatabase:
    Type: AWS::RDS::DBInstance
    DeletionPolicy: Snapshot
    UpdateReplacePolicy: Snapshot
    Properties:
      # ...

Metadata attache des données libres à une ressource. Il a un usage opérationnel à retenir : CloudFormation ne considère pas comme une mise à jour une modification portant uniquement sur une deletion policy, une update policy, une condition ou une déclaration d'output, donc une édition de template qui ne touche que cela produit « No updates to be performed ». Changer une valeur de metadata donne à CloudFormation quelque chose à voir, et la mise à jour passe.

CreationPolicy et UpdatePolicy, les deux attributs qui contrôlent l'attente de signaux et les mises à jour progressives d'Auto Scaling group, relèvent des opérations sur les stacks et apparaissent dans la leçon suivante.

Conseils pour l'examen

  • Ref renvoie l'identifiant principal de la ressource, Fn::GetAtt n'importe quel autre attribut par son nom. Si l'énoncé veut un ARN et que le Ref du type renvoie un nom, la réponse est GetAtt.
  • « Le même template doit fonctionner dans n'importe quelle Region ou n'importe quel compte » désigne les pseudo parameters et les types de parameter AWS ou SSM, et écarte les ID d'AMI et numéros de compte codés en dur.
  • Fn::ImportValue est limité au même compte et à la même Region. Un énoncé qui mentionne une seconde Region ou un second compte et demande un output de stack l'exclut et désigne Fn::GetStackOutput.
  • « Impossible de supprimer la stack, export utilisé par une autre stack » est le comportement attendu, pas un bug. Retirez d'abord l'import.
  • NoEcho masque les sorties describe. Toute réponse qui le traite comme du chiffrement, ou comme une protection des valeurs recopiées dans Outputs, est fausse.
  • Des valeurs sensibles dans un template, cela veut dire dynamic references (ssm-secure, secretsmanager), pas parameters.
  • La valeur par défaut Snapshot de RDS est une favorite. Lisez « aucune DeletionPolicy spécifiée » plus « RDS » comme un snapshot, et les mêmes mots plus « bucket S3 » comme une suppression.
  • Posez UpdateReplacePolicy à côté de DeletionPolicy dès qu'il y a des données. Les questions sur une base disparue pendant une mise à jour de routine testent exactement ce point.
  • « No updates to be performed » après avoir modifié seulement une deletion policy, une update policy, une condition ou un output est normal. Le remède est de changer une valeur de Metadata de ressource.
  • DependsOn sert à l'ordre qu'aucune référence n'exprime. L'attachement de l'internet gateway et l'Elastic IP est le couple canonique.

L'idée à emporter : un template est la description d'un état final souhaité plus un graphe de relations, et CloudFormation en déduit tout le reste, l'ordre de création, l'ordre de suppression, ce qui peut tourner en parallèle et ce qui doit changer quand vous modifiez une ligne.

Ce qui pose la question que cette leçon a délibérément laissée ouverte. Vous savez maintenant écrire un template qui crée correctement une stack la première fois. La leçon suivante traite de la deuxième fois et de toutes les suivantes : comment modifier une stack en marche sans la casser, comment voir ce qu'une mise à jour va faire avant qu'elle le fasse, et quoi faire quand quelqu'un a modifié vos ressources dans la console.