Tutoriel localstack (partie 3) - Automatiser le déploiement des ressources AWS

Localstack logo

Pré-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 :

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 :

  1. 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.

  2. 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
  1. 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.
  2. 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)
  3. 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.
  4. Le conteneur a besoin d’un accès à la socket docker pour écouter les évenements.
  5. 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 :

Error: error creating DynamoDB Table: RequestError: send request failed
caused by: Post http://localhost:4569/: dial tcp 127.0.0.1:4569: connect: connection refused

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:

error waiting for Lambda Function (counter) creation: unexpected state '', wanted target 'Active'. last error: %!s() </div> 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 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! ![Terraform apply screenshot](/public/img/localstack-part-3/terraform-apply-screenshot.png "terraform apply screenshot") Maintenant que terraform peut être exécuter à l'intérieur d'un conteneur, vous pouvez vous débarasser de la configuration des ports : ```yaml 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 : ```bash 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: ```yaml 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** : ```bash #!/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 : ```yaml 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 : ```bash 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](https://github.com/localstack/localstack/issues/2018) à ce sujet et je mettrais à jour cet article si besoin. > Mise à jour du 03/03/2020 - J'ai récemment créé une [pull request](https://github.com/localstack/localstack/commit/6c7f6a6b76ee4e1c03eeae7b4dfa7047a2b93cb3) 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](https://github.com/Ovski4/tutorials/tree/master/localstack-part-3) 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](https://github.com/Ovski4/tutorials) en guise de remerciement ! ⭐