AWS EC2 - Le script cfn-hup
Accueil Tags Recherche23 Mai 2023
AWS EC2 - Le script cfn-hup
Comment déclencher des mise à jour automatiques de vos instances EC2, suite à un changement de template CloudFormation ?Cet article s’inscrit dans un dossier sur la gestion de machines EC2 avec AWS CloudFormation. Merci de bien lire la section Introduction et disclaimers du premier article de ce dossier. Vous pouvez aussi retrouver l’intégralité du code utilisé en fin d’article.
Introduction
Le script cfn-hup
“HUP” est un raccourci pour “Hang UP”, qui est un héritage de la déjà lointaine époque des ports séries et du signal SIGHUP. Ce signal était utilisé pour indiquer la fermeture d’une ligne de communication série. Aujourd’hui le SIGHUP est surtout utilisé pour forcer les daemons à se redémarrer eux-mêmes, afin qu’ils puissent re-lire les fichiers de configuration.
Contrairement aux scripts que nous avons vus précédemment cfn-hup
est en effet un script daemon. Il vous faudra le lancer une première fois lors de la création de l’instance, puis il s’exécutera en arrière-plan. Ce script va détecter les modifications apportées aux méta-données de vos instances (dans votre template CloudFormation), et va vous permettre de déclencher des actions en réaction à ces changements.
On pourrait penser que cfn-hup
est superflu, et qu’il suffit de lancer cfn-init
régulièrement via un cron. C’est vrai dans les cas les plus simplistes, mais cette technique ne permet pas de répondre à la grande majorité des cas d’usages, nous verrons pourquoi dans la suite de cet article.
La mise en place de ce script va donc nécessiter plusieurs étapes :
- Le lancement du daemon
- La configuration de
cfn-hup
: quel template surveiller, et à quelle fréquence ? - La configuration des hooks : quelles commandes lancer, et lors de quels événements ?
Les jeux de configuration
Jusqu’ici nous avons toujours utilisé la clé config
dans nos méta-données AWS::CloudFormation::Init
afin de définir les commandes à lancer lors de la création de notre instance, mais à présent nous allons avoir un workflow un peu plus complexe :
- Une configuration pour Nginx
- Une configuration pour
cfn-hup
Notre objectif est que le serveur Nginx soit mis à jour automatiquement, on va donc vouloir lancer le premier ensemble de commandes à la création de l’instance, mais aussi lors de ses mises à jour. En revanche (pour l’exemple) nous souhaitons configurer cfn-hup
une seule fois, lors de la création de l’instance. Nous allons donc avoir ce qu’on appelle deux jeux de configuration :
- Un jeu à lancer suite à la création de l’instance (configuration de Nginx et de
cfn-hup
) - Un jeu à lancer suite à la mise à jour de l’instance (configuration de Nginx)
Pour répondre à ce besoin, les méta-données AWS::CloudFormation::Init
supportent une clé configSets
permettant de regrouper vos configurations dans des jeux (sets), afin de les lancer de manière indépendante. La clé config
n’étant utilisée que si configSets
n’est pas définie, nous allons commencer par renommer notre configuration config
en nginx
, pour ensuite ajouter la clé configSets
:
instance.instance.add_metadata("AWS::CloudFormation::Init", {
"configSets": {
"create": [
"nginx"]},
"nginx": {
# Installation du package Nginx
"commands": {
"01-nginx-install": {
"command": "sudo amazon-linux-extras install -y nginx1"}},
# Activation du service Nginx
"services": {
"sysvinit": {
"nginx": {
"enabled": True,
"ensureRunning": True,
"files": [
"/etc/nginx/nginx.conf",
"/usr/share/nginx/html/index.html"]}}}}
})
Pour récapituler ce changement, voici ce que nous avions avant :
- Une configuration appelée
config
(clé utilisée par défaut en l’absence de jeux de configuration)
Et voici ce que nous avons à présent :
- Un jeu de configuration appelé
create
- Une configuration appelée
nginx
(utilisé dans le jeucreate
)
Il faut aussi penser à modifier l’appel à cfn-init
, pour préciser quel jeu de configuration nous désirons lancer :
instance.add_user_data(dedent(f"""\
(
set +e
/opt/aws/bin/cfn-init -v \
--region eu-west-3 \
--stack {self.stack_name} \
--resource {instance.instance.logical_id} \
--configsets create
/opt/aws/bin/cfn-signal -e $? \
--region eu-west-3 \
--stack {self.stack_name} \
--resource {instance.instance.logical_id}
)"""))
Ce changement va nous permettre de mieux organiser nos configurations dans la suite de cet article, et je vous recommande de toujours utiliser les jeux de configuration ; ils permettent non seulement plus de souplesse pour vos évolutions futures, mais sont aussi beaucoup plus explicites et facilitent la lecture de ces configurations (très) verbeuses.
Mise en place de cfn-hup
Maintenant que tout est propre nous allons créer une nouvelle configuration nommée cfn-hup
(là-aussi, libre à vous de choisir le nom, il s’agit juste d’une clé), et l’ajouter au jeu de configuration create
:
instance.instance.add_metadata("AWS::CloudFormation::Init", {
"configSets": {
"create": [
"nginx",
"cfn-hup"]},
"nginx": { … },
"cfn-hup": {}
})
Je vais à présent détailler les trois étapes évoquées pour la mise en place complète du script cfn-hup
.
Lancement du daemon
Comme pour Nginx, nous allons utiliser la clé services.sysvinit
pour nous assurer que le daemon cfn-hup
soit lancé. Celui-ci dépendra des deux fichiers suivants, que nous allons voir juste après :
/etc/cfn/cfn-hup.conf
: configuration du script/etc/cfn/hooks.d/cfn-auto-reloader.conf
: configuration des hooks
instance.instance.add_metadata("AWS::CloudFormation::Init", {
"configSets": { … },
"nginx": { … },
"cfn-hup": {
# Activation du service cfn-hup
"services": {
"sysvinit": {
"cfn-hup": {
"enabled": True,
"ensureRunning": True,
"files": [
"/etc/cfn/cfn-hup.conf",
"/etc/cfn/hooks.d/cfn-auto-reloader.conf"]}}}}
})
Configuration du script
Ici on va préciser quel est le template CloudFormation de référence, et avec quelle fréquence cfn-hup
va devoir le surveiller. C’est le fichier /etc/cfn/cfn-hup.conf
qui est chargé de définir tout cela (voir la documentation pour les spécifications), voici (pour des raisons de lisibilité) à quoi nous voulons qu’il ressemble :
[main]
stack=<StackId>
region=eu-west-3
interval=5
verbose=true
On précise bien la stack à surveiller, ainsi qu’un intervale de 5 minutes entre chaque vérification. Voici le fichier ajouté aux méta-données de notre instance :
instance.instance.add_metadata("AWS::CloudFormation::Init", {
"configSets": {
"create": [
"nginx",
"cfn-hup"]},
"nginx": { … },
"cfn-hup": {
"files": {
# Fichier de configuration de cfn-hup
"/etc/cfn/cfn-hup.conf": {
"content": dedent(f"""\
[main]
stack={self.stack_id}
region=eu-west-3
interval=5
verbose=true"""),
"encoding": "plain",
"mode": "000400",
"owner": "root",
"group": "root"}},
# Activation du service cfn-hup
"services": {
"sysvinit": {
"cfn-hup": {
"enabled": True,
"ensureRunning": True,
"files": [
"/etc/cfn/cfn-hup.conf",
"/etc/cfn/hooks.d/cfn-auto-reloader.conf"]}}}}
})
Configuration des hooks
Nous avons déjà demandé à cfn-hup
de surveiller notre template CloudFormation, il faut à présent lui préciser quoi faire, et surtout quand. Le script fonctionne avec un système de hooks qui vont permettre d’écouter les événements suivants sur une ressource :
post.add
post.update
post.remove
C’est le fichier /etc/cfn/hooks.d/cfn-auto-reloader.conf
qui contiendra cette configuration (voir la documentation pour les spécifications), et une fois encore pour des raisons de lisibilité voici à quoi nous voulons qu’il ressemble :
[cfn-auto-reloader-hook]
triggers=post.update
path=Resources.<InstanceId>.Metadata.AWS::CloudFormation::Init
action=/opt/aws/bin/cfn-init -v \
--region eu-west-3 \
--stack <StackId> \
--resource <InstanceId> \
--configsets update
runas=root
C’est ce fichier qui est le plus important, et qui dépendra beaucoup de ce que vous voulez faire. Ici on désire réagir aux événements de mise à jour des méta-données de notre instance. Quand cet événement survient on lance le script cfn-init
, mais cette fois-ci en appelant un autre jeu de configuration nommé update
(qu’il va falloir créer pour l’occasion).
Voici le fichier ajouté aux méta-données de notre instance, ainsi que le changement des configSets
:
instance.instance.add_metadata("AWS::CloudFormation::Init", {
"configSets": {
"create": [
"nginx",
"cfn-hup"],
"update": [
"nginx"]},
"nginx": { … },
"cfn-hup": {
"files": {
# Fichier de configuration de cfn-hup
"/etc/cfn/cfn-hup.conf": {
"content": dedent(f"""\
[main]
stack={self.stack_id}
region=eu-west-3
interval=5
verbose=true"""),
"encoding": "plain",
"mode": "000400",
"owner": "root",
"group": "root"},
# Commandes à lancer lors d'un signal de mise à jour
"/etc/cfn/hooks.d/cfn-auto-reloader.conf": {
"content": dedent(f"""\
[cfn-auto-reloader-hook]
triggers=post.update
path=Resources.{instance.instance.logical_id}.Metadata.AWS::CloudFormation::Init
action=/opt/aws/bin/cfn-init -v \
--region eu-west-3 \
--stack {self.stack_id} \
--resource {instance.instance.logical_id} \
--configsets update
runas=root"""),
"encoding": "plain",
"mode": "000400",
"owner": "root",
"group": "root"}},
# Activation du service cfn-hup
"services": {
"sysvinit": {
"cfn-hup": {
"enabled": True,
"ensureRunning": True,
"files": [
"/etc/cfn/cfn-hup.conf",
"/etc/cfn/hooks.d/cfn-auto-reloader.conf"]}}}}
})
Nous pouvons à présent provisionner ces changements avec cdk deploy
, et parce que nous avons modifié les données utilisateur, l’instance va à nouveau être remplacée. Profitez-en donc pour récupérer la nouvelle adresse IP publique :
# Outputs:
# WorkshopStack.InstanceLogicalId = InstanceC1063A87e4b92a55b80d62ff
# WorkshopStack.InstancePublicIp = 13.38.71.54
Déploiement d’une modification
En accédant à notre serveur en HTTP sur la nouvelle adresse IP (http://13.38.71.54 dans mon exemple), on retrouve la page d’accueil par défaut de Nginx. Maintenant que cfn-hup
est en place nous allons modifier cette page via les méta-données, et voir si ce changement est bien répercuté sur notre instance. Ajoutons une section files
à notre configuration nginx
, pour changer le contenu de cette page d’accueil :
instance.instance.add_metadata("AWS::CloudFormation::Init", {
"configSets": { … },
"nginx": {
# Installation du package Nginx
"commands": {
"01-nginx-install": {
"command": "sudo amazon-linux-extras install -y nginx1"}},
# Modification de la page par défaut de Nginx
"files": {
"/usr/share/nginx/html/index.html": {
"content": "<title>Hello World</title><h1>Hello World</h1>",
"mode": "000644",
"owner": "root",
"group": "root"}},
# Activation du service Nginx
"services": {
"sysvinit": {
"nginx": {
"enabled": True,
"ensureRunning": True,
"files": [
"/etc/nginx/nginx.conf",
"/usr/share/nginx/html/index.html"]}}}},
"cfn-hup": { … }
})
En lançant la commande cdk diff
on constate qu’il n’y a pas de changement d’instance prévu, juste un ajout de méta-données :
# Stack WorkshopStack
# Resources
# [~] AWS::EC2::Instance Instance InstanceC1063A87e4b92a55b80d62ff
# └─ [~] Metadata
# └─ [~] .AWS::CloudFormation::Init:
# └─ [~] .nginx:
# └─ [+] Added: .files
On lance donc cdk deploy
. Vous pourrez voir que la mise à jour sera très rapide (une vingtaine de secondes) : comme expliqué en début d’article, une modification de méta-données n’a sur le moment aucune incidence sur notre instance, nous avons donc ajouté quelques lignes de texte dans un template CloudFormation, jusqu’ici rien de plus.
Au maximum 5 minutes plus tard, vous devriez voir cette nouvelle page d’accueil au design dans le plus pur esprit du brutalisme apparaître 🎉
Conclusion
Nous voila arrivés à la fin de ce dossier qui couvre les bases de la gestion d’instance EC2 avec CloudFormation. Sa rédaction aura pris beaucoup de temps, j’ai apporté un soin tout particulier à la stack CloudFormation et au code qui, je l’espère, devraient vous avoir permis de suivre l’ensemble des articles plus facilement en reproduisant toutes les opérations vous-mêmes.
Vous voici à présent en mesure de provisioner et configurer des instances EC2 sans aucune intervention manuelle. Il y aura très probablement d’autres articles sur EC2 dans le futur, qui s’attarderont plus en détails sur certaines techniques et/ou fonctionnalités, restez dans le coin…
Voir l'intégralité du code
from aws_cdk import (
Stack,
CfnOutput,
CfnCreationPolicy,
CfnResourceSignal,
aws_ec2 as ec2,
)
from constructs import Construct
from textwrap import dedent
class WorkshopStack(Stack):
def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
super().__init__(scope, construct_id, **kwargs)
vpc = ec2.Vpc(self, "Vpc",
subnet_configuration = [
ec2.SubnetConfiguration(
name = "public",
subnet_type = ec2.SubnetType.PUBLIC,
cidr_mask = 24)],
max_azs = 1)
security_group = ec2.SecurityGroup(self, "InstanceSecurityGroup",
vpc = vpc)
security_group.add_ingress_rule(ec2.Peer.any_ipv4(), ec2.Port.tcp(22))
security_group.add_ingress_rule(ec2.Peer.any_ipv4(), ec2.Port.tcp(80))
cfn_key_pair = ec2.CfnKeyPair(self, "KeyPair",
key_name = "ssh-key-workshop",
key_type = "ed25519")
instance = ec2.Instance(self, "Instance",
# Type d'instance : t2.micro
instance_type = ec2.InstanceType.of(
instance_class = ec2.InstanceClass.T2,
instance_size = ec2.InstanceSize.MICRO),
# AMI à utiliser
machine_image = ec2.MachineImage.generic_linux({
"eu-west-3": "ami-01fde5e5b31e98551"}),
# VPC dans lequel déployer l'instance
vpc = vpc,
# Groupe de sécurité pour autoriser le trafic sur le port 22
security_group = security_group,
# SSH key to use
key_name = cfn_key_pair.key_name,
# Un changement de user-data doit provoquer un changement d'instance
user_data_causes_replacement = True)
instance.add_user_data(dedent(f"""\
(
set +e
/opt/aws/bin/cfn-init -v \
--region eu-west-3 \
--stack {self.stack_name} \
--resource {instance.instance.logical_id} \
--configsets create
/opt/aws/bin/cfn-signal -e $? \
--region eu-west-3 \
--stack {self.stack_name} \
--resource {instance.instance.logical_id}
)"""))
cfn_instance = instance.node.default_child
cfn_instance.cfn_options.creation_policy = CfnCreationPolicy(
resource_signal = CfnResourceSignal(
count = 1,
timeout = "PT5M"))
instance.instance.add_metadata("AWS::CloudFormation::Init", {
"configSets": {
"create": [
"nginx",
"cfn-hup"],
"update": [
"nginx"]},
"nginx": {
# Installation du package Nginx
"commands": {
"01-nginx-install": {
"command": "sudo amazon-linux-extras install -y nginx1"}},
# Modification de la page par défaut de Nginx
"files": {
"/usr/share/nginx/html/index.html": {
"content": "<title>Hello World</title><h1>Hello World</h1>",
"mode": "000644",
"owner": "root",
"group": "root"}},
# Activation du service Nginx
"services": {
"sysvinit": {
"nginx": {
"enabled": True,
"ensureRunning": True,
"files": [
"/etc/nginx/nginx.conf",
"/usr/share/nginx/html/index.html"]}}}},
"cfn-hup": {
"files": {
# Fichier de configuration de cfn-hup
"/etc/cfn/cfn-hup.conf": {
"content": dedent(f"""\
[main]
stack={self.stack_id}
region=eu-west-3
interval=5
verbose=true"""),
"encoding": "plain",
"mode": "000400",
"owner": "root",
"group": "root"},
# Commandes à lancer lors d'un signal de mise à jour
"/etc/cfn/hooks.d/cfn-auto-reloader.conf": {
"content": dedent(f"""\
[cfn-auto-reloader-hook]
triggers=post.update
path=Resources.{instance.instance.logical_id}.Metadata.AWS::CloudFormation::Init
action=/opt/aws/bin/cfn-init -v \
--region eu-west-3 \
--stack {self.stack_id} \
--resource {instance.instance.logical_id} \
--configsets update
runas=root"""),
"encoding": "plain",
"mode": "000400",
"owner": "root",
"group": "root"}},
# Activation du service cfn-hup
"services": {
"sysvinit": {
"cfn-hup": {
"enabled": True,
"ensureRunning": True,
"files": [
"/etc/cfn/cfn-hup.conf",
"/etc/cfn/hooks.d/cfn-auto-reloader.conf"]}}}}
})
# Affiche l'identifiant logique de l'instance
CfnOutput(self, "InstanceLogicalId",
value = instance.instance.logical_id)
# Affiche l'adresse IP publique de l'instance
CfnOutput(self, "InstancePublicIp",
value = instance.instance_public_ip)
Liens
Référence AWS - cfn-hup
Référence AWS - Jeux de configuration AWS::CloudFormation::Init
Référence AWS - UpdateStack