Tutoriel localstack (partie 3) - Automatiser le déploiement des ressources AWS
10 Feb 2020Pré-requis
J’ai écris les 2 premiers articles sur Localstack bien avant celui-ci. Localstack a été amélioré depuis. Certaines problèmes ont été résolus et certaines fonctionnalités sont apparues. Les articles précédents ont été mis à jour pour refléter les changements. Si vous arrivez sur cette page sans les avoir lu, je vous conseille fortement de le faire et de revenir sur cette page après.
Pour suivre ce tutoriel il vous faudra :
- Les fichiers de configurations de la deuxième partie (fichier lambda.zip inclu)
- Docker et docker-compose installés
Vérifier que le réseau docker localstack-tutorial soit présent. Si ce n’est pas le cas, créez-le avec la commande docker network create localstack-tutorial
.
Il n’y a même plus besoin d’installer Terraform ou le cli AWS : ici tout pourra fonctionner sous docker.
Si vous êtes pressé, tous les fichiers créés dans ce tuto sont disponibles sur ce repository github avec des instructions pour tout installer en accéléré.
Introduction
Quel est l’objectif de ce tutoriel ? Comme vu précédemment, même si Terraform aide à déployer des ressources AWS dans localstack, il y a encore quelques aspects négatifs:
- Toutes les ressources ne sont pas sauvegardés sur le disque via des volumes docker. Il faut appliquer la configuration Terraform à chaque redémarrage du projet.
- Exécuter des fonctions lambdas prend du temps à la première invocation. Ce n’est pas un problème pour des fonctions asychrones. Cependant, pour des fonctions dont la réponse est bloquante, ce n’est pas si simple. De plus, après 10 minutes d’inactivité le conteneur est tué automatiquement par localstack.
Pour pallier à ces problèmes je suis parvenu à implémenter 2 solutions :
-
En utilisant les évenements docker on peut déclencher la commande
terraform apply
immédiatement après le démarrage de localstack. En tant que développeur il n’est plus necessaire de penser à déployer les ressources à chaque fois. -
On utilisera notre propre conteneur localstack pour empêcher la destruction des conteneurs de fonctions lambda. (mise à jour du 3 mars 2020 : ma contribution à localstack a été acceptée et il n’est plus necessaire de créer une autre image. Plus d’information ci-dessous.)
Implémentons tout cela.
Création du conteneur docker-events-listener
Nous allons ici créé un conteneur docker qui écoutera les évenements docker. Dès que localstack démarre, ce conteneur se chargera de déployer les ressources AWS avec Terraform.
Le service docker-compose
Dans le fichier docker-compose.yml, ajouter le service docker-events-listener.
services:
localstack:
...
container_name: localstack # 1
depends_on:
- docker-events-listener # 2
docker-events-listener:
build:
context: docker-events-listener-build # 3
volumes:
- /var/run/docker.sock:/var/run/docker.sock # 4
- ./terraform:/opt/terraform/ # 5
- On précise le nom du conteneur localstack. Ce nom sera nécessaire pour savoir quels évenements proviennent du conteneur localstack lors de l’écoute des évenements docker.
- Le service docker-events-listener doit démarrer avant localstack (sinon l’évenement de démarrage du conteneur localstack sera déclenché avant même que le service docker-events-listener n’ai pu l’écouter)
- On créé un un dossier appelé docker-events-listener-build. Il s’agira du contexte de build de l’image docker et contiendra donc tous les fichiers nécessaire à la construction de l’image.
- Le conteneur a besoin d’un accès à la socket docker pour écouter les évenements.
- Le conteneur a besoin d’accéder aux fichiers terraform pour pouvoir effectuer le déploiement
Fichiers terraform
Comme défini dans le fichier docker-compose.yml, créeons un dossier nommé terraform, et déplacons les fichiers localstack.tf et lambda.zip à l’intérieur.
mkdir terraform
mv lambda.zip terraform
mv localstack.tf terraform
Les fichiers de build
Le conteneur va exécuter des commands terraform et du CLI AWS. Il nous faut donc les installer. Le CLI AWS tirera sa configuration des fichiers .txt.
Si ce n’est pas dejà fait, créez le répertoire docker-events-listener-build. Puis créez les fichiers suivants à l’intérieur:
Le fichier aws_config.txt avec le contenu suivant:
[default]
output = json
region = ap-southeast-2
Puis le fichier aws_credentials.txt avec le contenu suivant:
[default]
aws_secret_access_key = fake
aws_access_key_id = fake
La partie la plus importante concerne le script bash qui constituera le processus principal du conteneur. Ce script écoute les évenements docker et effectue des actions en conséquences. Dans notre cas, il exécutera terraform apply
dès que localstack démarre.
Ci-dessous le contenu du script appelé listen-docker-events.sh :
#!/bin/bash
docker events --filter 'event=create' --filter 'event=start' --filter 'type=container' --format '{{.Actor.Attributes.name}} {{.Status}}' | while read event_info
do
event_infos=($event_info)
container_name=${event_infos[0]}
event=${event_infos[1]}
echo "$container_name: status = ${event}"
if [[ $container_name = "localstack" ]] && [[ $event == "start" ]]; then
sleep 20 # laissons 20 secondes à localstack le temps de démarrer
terraform init
terraform apply --auto-approve
echo "La configuration terraform à été appliqué"
fi
done
Grâce aux options –filter, seuls les événements des conteneurs qui sont démarrés ou bien crées seront écoutés, et seuls les noms des conteneurs suivi du nom de l’événement seront affichés.
Pour finir avec la création des fichiers docker, voici le contenu du Dockerfile.
FROM docker:19.03.5
RUN apk update && \
apk upgrade && \
apk add --no-cache bash wget unzip
# Installe le CLI AWS
RUN echo -e 'http://dl-cdn.alpinelinux.org/alpine/edge/main\nhttp://dl-cdn.alpinelinux.org/alpine/edge/community\nhttp://dl-cdn.alpinelinux.org/alpine/edge/testing' > /etc/apk/repositories && \
wget "s3.amazonaws.com/aws-cli/awscli-bundle.zip" -O "awscli-bundle.zip" && \
unzip awscli-bundle.zip && \
apk add --update groff less python curl && \
rm /var/cache/apk/* && \
./awscli-bundle/install -i /usr/local/aws -b /usr/local/bin/aws && \
rm awscli-bundle.zip && \
rm -rf awscli-bundle
COPY aws_credentials.txt /root/.aws/credentials
COPY aws_config.txt /root/.aws/config
# Installe Terraform
RUN wget https://releases.hashicorp.com/terraform/0.12.20/terraform_0.12.20_linux_amd64.zip \
&& unzip terraform_0.12.20_linux_amd64 \
&& mv terraform /usr/local/bin/terraform \
&& chmod +x /usr/local/bin/terraform
RUN mkdir -p /opt/terraform
WORKDIR /opt/terraform
COPY listen-docker-events.sh /var/listen-docker-events.sh
CMD ["/bin/bash", "/var/listen-docker-events.sh"]
Si les 4 fichiers ci-dessus sont bien présents dans le dossier docker-events-listener-build, continuez à lire.
Les événements docker en action
Buildons la nouvelle images et démarrons les conteneurs. Pour être que tout est à zéro avant vous pouvez exécuter la commande docker-compose down -v
.
docker-compose build
docker-compose up -d
docker-compose logs -f docker-events-listener
Si vous n’avez pas modifié les fichiers terraform, vous devriez voir l’erreur suivante au bout de quelques secondes :
Que ce passe-t-il ? Si vous jetez un oeil au fichier localstack.tf, vous allez voir que terraform est configuré pour atteindre dynamodb à l’adresse localhost, qui est le conteneur docker-events-listener lui même. Évidemment, dynamodb n’est pas disponible dans le conteneur docker-events-listener, mais dans le conteneur localstack.
Pour résoudre ce problème, mettons à jour le fichier localstack.tf et remplaçons chaque occurence de localhost avec localstack. Pourquoi localstack ? Car il s’agit du nom du service localstack dans le fichier docker-compose.
Comme expliqué dans le premier tutoriel, la documentation docker spécifie qu’un conteneur créé à partir d’un service docker-compose sera atteignable par tous les conteneurs partageant un réseau en commun, et pourra être découvert avec le même hostname que son nom de service. (Par défaut docker-compose créé un réseau commun à tous les conteneurs définis dans un fichier docker-compose.yml)
Essayons à nouveau après modification du fichier localstack.tf :
docker-compose down -v
docker-compose build
docker-compose up -d
docker-compose logs -f docker-events-listener
Si vous obtenez l’erreur suivante:
Vous devriez utiliser une version plus ancienne du provider aws pour l’instant. Dans ce cas, utiliser le bloc suivant dans le fichier localstack.tf :
terraform {
required_providers {
aws = "~> 2.39.0"
}
}
Après ceci, vous devriez avoir la commande terraform apply
du conteneur fonctionner parfaitement en observant les logs!
Maintenant que terraform peut être exécuter à l’intérieur d’un conteneur, vous pouvez vous débarasser de la configuration des ports :
ports:
- 4569:4569 # dynamodb
- 4574:4574 # lamba
Pour interagir avec ces services via terraform or le CLI AWS, il est possible d’utiliser la commande docker exec
. Essayez ceci :
docker exec -it localstack-part-3_docker-events-listener_1 aws lambda invoke --function-name counter --endpoint-url=http://localstack:4574 --payload '{"id": "test"}' output.txt
docker exec -it localstack-part-3_docker-events-listener_1 aws dynamodb scan --endpoint-url http://localstack:4569 --table-name table_1
Création des conteneurs lambdas au démarrage
Cette deuxième partie correspond à un besoin très spécifique dont vous n’aurez sans doute pas besoin.
Dans mon environnement de développement, les fonctions lambdas sont invoquées de manière asynchrone à la suite de requêtes HTTP. La première fois qu’une lambda est appelée, localstack créé un conteneur. Cette étapes prend plusieurs secondes. Entre le moment ou la requête HTTP est envoyée et le moment le conteneur lambda est prêt, la requête HTTP peut expirer (timeout). Grâce à la variable d’environnement LAMBDA_EXECUTOR avec la valeur docker-reuse, localstack garde le conteneur en vie pendant 10 minutes. Les prochains appels sont donc bien plus rapides.
Cette fois aussi j’ai choisi d’utiliser les evénements docker pour améliorer cela. À l’aide des variables d’environnement, on peut configurer le comportement du conteneur docker-events-listener depuis le fichier docker-compose.
Démarrons par le code. Les explications suivront.
Voici le fichier docker-compose.yml mis à jour:
services:
localstack:
...
docker-events-listener:
...
environment:
APPLY_TERRAFORM_ON_START: "true"
INVOKE_LAMBDAS_ON_START: Lambda1 Lambda2 Lambda3
Et le script listen-docker-events.sh :
#!/bin/bash
docker events --filter 'event=create' --filter 'event=start' --filter 'type=container' --format '{{.Actor.Attributes.name}} {{.Status}}' | while read event_info
do
event_infos=($event_info)
container_name=${event_infos[0]}
event=${event_infos[1]}
echo "$container_name: status = ${event}"
if [[ $APPLY_TERRAFORM_ON_START == "true" ]] && [[ $container_name = "localstack" ]] && [[ $event == "start" ]]; then
terraform init
terraform apply --auto-approve
echo "The terraform configuration has been applied."
if [[ -n $INVOKE_LAMBDAS_ON_START ]]; then
echo "Invoking the lambda functions specified in the INVOKE_LAMBDAS_ON_START env variable"
while IFS=' ' read -ra lambdas; do
for lambda in "${lambdas[@]}"; do
echo "Invoking ${lambda}"
aws lambda invoke --function-name ${lambda} --endpoint-url=http://localstack:4574 output.txt &
done
done <<< "$INVOKE_LAMBDAS_ON_START"
fi
fi
done
- Le premier ajout est la variable d’environnement APPLY_TERRAFORM_ON_START. C’est utile pour permettre de désactiver le déploiement automatique avec Terraform au cas où vous voudriez appliquer la configuration manuellement.
- Le deuxième ajout concerne la variable d’environnement INVOKE_LAMBDAS_ON_START. Pour chaque mot séparé par un espace, une fonction lambda est invoquée (son nom correspondant au mot).
En mettant à jour le fichier docker-compose.yml pour fonctionner avec la lambda counter qui nous suit depuis le début de cette série de tutoriels, voici ce que l’on obtient :
services:
localstack:
...
docker-events-listener:
...
environment:
APPLY_TERRAFORM_ON_START: "true"
INVOKE_LAMBDAS_ON_START: counter
Redémarrons tout à partir de zéro une fois encore :
docker-compose down -v
docker-compose build
docker-compose up -d
Attendez quelques secondes (peut être plusieurs minutes…), puis exécutez docker ps
. Vous devriez avoir un conteneur lambda dans la liste !
💡 Utiliser des labels docker serait une meilleure approche pour implémenter cette solution, et résulterait en un script bash plus propre.
Empêcher la destruction des conteneurs lambdas
Même si nous avons automatisé la création des conteneurs lambda, quel est l’intérêt si ceux-ci sont détruits après 10 minutes d’inactivité ? Un paramètrage de ce genre de comportement devrait être possible. J’ai ouvert une discussion sur github à ce sujet et je mettrais à jour cet article si besoin.
Mise à jour du 03/03/2020 - J’ai récemment créé une pull request qui sera disponible dans la version 0.10.8 de Terraform. Il suffit de définir la variable d’environnement LAMBDA_REMOVE_CONTAINERS avec la valeur false pour empêcher la destruction automatique des conteneurs lambdas
Jetez un coup d’oeil à la solution finale sur github pour voir comment tous les éléments s’agencent entre eux!
Conclusion
Grâce aux événements docker, il est possible de mettre en place de nombreuses fonctionnalités qui semble impossible à première vue. D’un autre côté, la configuration devient particulièrement complexe. Le déploiement automatique cache la complexité des processus qui s’exécutent dans les conteneurs. Quand tous fonctionne, c’est une aubaine. Mais dès lors que quelque chose dérape, cela peut prendre du temps de comprendre et de résoudre les problèmes car il est nécessaire de comprendre comment tout fonctionne.
Ayez cela à l’esprit et choisissez par vous même si vous voulez continuer dans cette direction!
Si cet article vous a rendu service, n’hésitez pas à starrer ce repo en guise de remerciement ! ⭐