De la vie avec Kubernetes: comment nous avons supprimé le SGBD (et pas seulement) des environnements de révision aux statiques



Remarque : cet article ne prétend pas être une meilleure pratique. Il décrit l'expérience d'une implémentation spécifique d'une tâche d'infrastructure en termes d'utilisation de Kubernetes et Helm, qui peut être utile pour résoudre des problèmes connexes.

L'utilisation d'environnements de révision dans CI / CD peut être très utile, à la fois pour les développeurs et les ingénieurs système. Synchronisons d'abord les idées générales à leur sujet:

  1. Les environnements de révision peuvent être créés à partir de branches distinctes dans des référentiels Git définis par les développeurs (les soi-disant branches de fonctionnalités).
  2. Ils peuvent avoir des instances SGBD distinctes, des processeurs de file d'attente, des services de mise en cache, etc. - en général, tout pour la reproduction intégrale de l'environnement de production.
  3. Ils permettent un développement parallèle, accélérant considérablement la sortie de nouvelles fonctionnalités dans l'application. Dans le même temps, des dizaines de ces environnements peuvent être nécessaires chaque jour, c'est pourquoi la vitesse de leur création est critique.

A l'intersection des deuxième et troisième points, des difficultés surviennent souvent: l'infrastructure étant très différente, ses composants peuvent être déployés longtemps. Ce temps passé, par exemple, comprend la restauration de la base de données à partir d'une sauvegarde déjà préparée *. L'article traite de la manière fascinante dont nous avons utilisé une fois pour résoudre un tel problème.

* Soit dit en passant, en particulier sur les grands vidages de base de données dans ce contexte, nous avons déjà écrit dans le document sur l'accélération de la base de données d'amorçage .)

Le problème et la façon de le résoudre


Dans l'un des projets, nous avons été chargés de «créer un point d'entrée unique pour les développeurs et les ingénieurs d'assurance qualité». Cette formulation cache techniquement les éléments suivants:

  1. Pour simplifier le travail des ingénieurs QA et de certains autres employés, supprimez toutes les bases de données (et les hôtes correspondants) utilisées dans la revue, dans un environnement séparé - statique -. Pour des raisons qui prévalaient dans le projet, cette façon d'interagir avec eux était optimale.
  2. Réduisez le temps de création de l'environnement de révision. Tout le processus de leur création à partir de zéro est impliqué, c'est-à-dire y compris le clonage de bases de données, les migrations, etc.

Du point de vue de la mise en œuvre, le problème principal se résume à assurer l'idempotence lors de la création et de la suppression des environnements de révision. Pour y parvenir, nous avons modifié le mécanisme de création d'environnements de révision en migrant d'abord les services PostgreSQL, MongoDB et RabbitMQ vers un environnement statique. Statique fait référence à un tel environnement «permanent» qui ne sera pas créé à la demande de l'utilisateur (comme c'est le cas avec les environnements de révision).

Important! L'approche avec un environnement statique est loin d'être idéale - pour ses défauts spécifiques, voir la fin de l'article. Cependant, nous partageons cette expérience en détail, car elle peut être plus ou moins applicable à d'autres tâches, et en même temps servir d'argument lors de l'examen des problèmes de conception d'infrastructure.

Ainsi, la séquence d'actions dans la mise en œuvre:

  • Lors de la création d'un environnement de révision, les opérations suivantes doivent se produire une fois: la création de bases de données dans deux SGBD (MongoDB et PostgreSQL), la restauration de bases de données à partir d'une sauvegarde / d'un modèle et la création de vhost dans RabbitMQ. Cela nécessitera un moyen pratique de charger les décharges de courant. (Si vous aviez déjà examiné des environnements, vous avez probablement déjà une solution prête à l'emploi pour cela.)
  • Une fois l'environnement d'examen terminé, vous devez supprimer la base de données et l'hôte virtuel dans RabbitMQ.

Dans notre cas, l'infrastructure fonctionne dans le cadre de Kubernetes (utilisant Helm). Par conséquent, pour la mise en œuvre des tâches ci-dessus, les crochets de barre étaient excellents . Ils peuvent être effectués à la fois avant la création de tous les autres composants de la version Helm et / ou après leur suppression. Donc:

  • pour la tâche d'initialisation, nous utiliserons un hook pre-installpour le lancer avant de créer toutes les ressources dans la version;
  • pour la tâche de suppression, un crochet post-delete.

Passons aux détails de l'implémentation.

Mise en œuvre pratique


Dans la version originale, ce projet utilisait un seul Job, composé de trois conteneurs. Bien sûr, ce n'est pas tout à fait pratique, car le résultat est un grand manifeste difficile à lire. Par conséquent, nous l'avons divisé en trois petits emplois.

Ce qui suit est une liste pour PostgreSQL, et les deux autres (MongoDB et RabbitMQ) sont identiques dans la structure du manifeste:

{{- if .Values.global.review }}
---
apiVersion: batch/v1
kind: Job
metadata:
  name: db-create-postgres-database
  annotations:
    "helm.sh/hook": "pre-install"
    "helm.sh/hook-weight": "5"
spec:
  template:
    metadata:
      name: init-db-postgres
    spec:
      volumes:
      - name: postgres-scripts
        configMap:
          defaultMode: 0755
          name: postgresql-configmap
      containers:
      - name: init-postgres-database
        image: private-registry/postgres 
        command: ["/docker-entrypoint-initdb.d/01-review-load-dump.sh"]
        volumeMounts:
        - name: postgres-scripts
          mountPath: /docker-entrypoint-initdb.d/01-review-load-dump.sh
          subPath: review-load-dump.sh
        env:
{{- include "postgres_env" . | indent 8 }}
      restartPolicy: Never
{{- end }}

Commentaires sur le contenu du manifeste:

  1. Job review-. review CI/CD Helm- (. if .Values.global.review ).
  2. Job — , ConfigMap. , , . , hook-weight.
  3. cURL , PostgreSQL, .
  4. PostgreSQL : , shell- .

PostgreSQL


Le plus intéressant est dans le script shell ( review-load-dump.sh) déjà mentionné dans la liste . Quelles sont les options générales pour restaurer une base de données dans PostgreSQL?

  1. Récupération "standard" à partir d'une sauvegarde;
  2. Récupération à l'aide de modèles .

Dans notre cas, la différence entre les deux approches réside principalement dans la vitesse de création d'une base de données pour le nouvel environnement. Dans le premier - nous chargeons le vidage de la base de données et le restaurons avec pg_restore. Et avec nous, cela se produit plus lentement que la deuxième méthode, donc le choix correspondant a été fait.

Utilisation de la deuxième option ( récupération avec des modèles), vous pouvez cloner la base de données au niveau physique sans lui envoyer de données à distance depuis le conteneur dans un autre environnement, ce qui réduit le temps de récupération. Cependant, il existe une limitation: vous ne pouvez pas cloner une base de données dans laquelle les connexions actives restent. Étant donné que nous utilisons stage comme environnement statique (et non un environnement d'examen séparé), nous devons créer une deuxième base de données et la convertir en modèle, en la mettant à jour quotidiennement (par exemple, le matin). Un petit CronJob a été préparé pour cela:

---
apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: update-postgres-template
spec:
  schedule: "50 4 * * *"
  concurrencyPolicy: Forbid
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 3
  startingDeadlineSeconds: 600
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: Never
          imagePullSecrets:
          - name: registrysecret
          volumes:
          - name: postgres-scripts
            configMap:
              defaultMode: 0755
              name: postgresql-configmap-update-cron
          containers:
          - name: cron
            command: ["/docker-entrypoint-initdb.d/update-postgres-template.sh"]
          image: private-registry/postgres 
            volumeMounts:
            - name: postgres-scripts
              mountPath: /docker-entrypoint-initdb.d/update-postgres-template.sh
              subPath: update-postgres-template.sh
            env:
{{- include "postgres_env" . | indent 8 }}

Le manifeste ConfigMap complet contenant le script n'a probablement pas beaucoup de sens (signalez-le dans les commentaires si ce n'est pas le cas). Au lieu de cela, je vais donner la chose la plus importante - un script bash:

#!/bin/bash -x

CREDENTIALS="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}/postgres"

psql -d "${CREDENTIALS}" -w -c "REVOKE CONNECT ON DATABASE ${POSTGRES_DB_TEMPLATE} FROM public"
psql -d "${CREDENTIALS}" -w -c "SELECT pg_terminate_backend(pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = '${POSTGRES_DB_TEMPLATE}'"

curl --fail -vsL ${HOST_FORDEV}/latest_${POSTGRES_DB_STAGE}.psql -o /tmp/${POSTGRES_DB}.psql

psql -d "${CREDENTIALS}" -w -c "ALTER DATABASE ${POSTGRES_DB_TEMPLATE} WITH is_template false allow_connections true;"
psql -d "${CREDENTIALS}" -w -c "DROP DATABASE ${POSTGRES_DB_TEMPLATE};" || true
psql -d "${CREDENTIALS}" -w -c "CREATE DATABASE ${POSTGRES_DB_TEMPLATE};" || true
pg_restore -U ${POSTGRES_USER} -h ${POSTGRES_HOST} -w -j 4 -d ${POSTGRES_DB_TEMPLATE} /tmp/${POSTGRES_DB}.psql

psql -d "${CREDENTIALS}" -w -c "ALTER DATABASE ${POSTGRES_DB_TEMPLATE} WITH is_template true allow_connections false;"

rm -v /tmp/${POSTGRES_DB}.psql

Vous pouvez restaurer plusieurs bases de données à la fois à partir d'un modèle sans aucun conflit. L'essentiel est que les connexions à la base de données soient interdites et que la base de données elle-même soit un modèle. Cela se fait à l'avant-dernière étape.

Le manifeste contenant le script shell pour restaurer la base de données s'est avéré comme ceci:

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: postgresql-configmap
  annotations:
    "helm.sh/hook": "pre-install"
    "helm.sh/hook-weight": "1"
    "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
data:
  review-load-dump.sh: |
    #!/bin/bash -x
    
 
 
    CREDENTIALS="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}/postgres"

    if [ "$( psql -d "${CREDENTIALS}" -tAc "SELECT CASE WHEN EXISTS (SELECT * FROM pg_stat_activity WHERE datname = '${POSTGRES_DB}' LIMIT 1) THEN 1 ELSE 0 END;" )" = '1' ]
      then
          echo "Open connections has been found in ${POSTGRES_DB} database, will drop them"
          psql -d "${CREDENTIALS}" -c "SELECT pg_terminate_backend(pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = '${POSTGRES_DB}' -- AND pid <> pg_backend_pid();"
      else
          echo "No open connections has been found ${POSTGRES_DB} database, skipping this stage"
    fi

    psql -d "${CREDENTIALS}" -c "DROP DATABASE ${POSTGRES_DB}"

    if [ "$( psql -d "${CREDENTIALS}" -tAc "SELECT 1 FROM pg_database WHERE datname='${POSTGRES_DB}'" )" = '1' ]
      then
          echo "Database ${POSTGRES_DB} still exists, delete review job failed"
          exit 1
      else
          echo "Database ${POSTGRES_DB} does not exist, skipping"
    fi


    psql ${CREDENTIALS} -d postgres -c 'CREATE DATABASE ${POSTGRES_DB} TEMPLATE "loot-stage-copy"'

Apparemment, ils sont impliqués ici hook-delete-policy. Les détails sur l'application de ces politiques sont écrits ici . Dans le manifeste donné, nous utilisons before-hook-creation,hook-succeededce qui permet de remplir les conditions suivantes: supprimer l'objet précédent avant de créer un nouveau hook et supprimer uniquement lorsque le hook a réussi.

Nous supprimerons la base de données de ce ConfigMap:

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: postgresql-configmap-on-delete
  annotations:
    "helm.sh/hook": "post-delete, pre-delete"
    "helm.sh/hook-weight": "1"
    "helm.sh/hook-delete-policy": before-hook-creation
data:
  review-delete-db.sh: |
    #!/bin/bash -e

    CREDENTIALS="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}/postgres"

    psql -d "${CREDENTIALS}" -w postgres -c "DROP DATABASE ${POSTGRES_DB}"

Bien que nous l'avons déplacé vers une ConfigMap distincte, il peut être placé dans une ConfigMap régulière command. Après tout, il peut être fabriqué en un seul revêtement sans compliquer l'apparence du manifeste lui-même.

Si l'option avec les modèles PostgreSQL pour une raison quelconque ne convient pas ou ne convient pas, vous pouvez revenir au chemin de récupération "standard" mentionné ci-dessus en utilisant la sauvegarde . L'algorithme sera trivial:

  1. Chaque nuit, une sauvegarde de la base de données est effectuée afin de pouvoir la télécharger depuis le réseau local du cluster.
  2. Au moment de la création de l'environnement de révision, la base de données est chargée et restaurée à partir du vidage.
  3. Lorsque le vidage est déployé, toutes les autres actions sont effectuées.

Dans ce cas, le script de récupération deviendra approximativement comme suit:

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: postgresql-configmap
  annotations:
    "helm.sh/hook": "pre-install"
    "helm.sh/hook-weight": "1"
    "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
data:
  review-load-dump.sh: |
    #!/bin/bash -x

    CREDENTIALS="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}/postgres"
    psql -d "${CREDENTIALS}" -w -c "DROP DATABASE ${POSTGRES_DB}" || true
    psql -d "${CREDENTIALS}" -w -c "CREATE DATABASE ${POSTGRES_DB}"

    curl --fail -vsL ${HOST_FORDEV}/latest_${POSTGRES_DB_STAGE}.psql -o /tmp/${POSTGRES_DB}.psql

    psql psql -d "${CREDENTIALS}" -w -c "CREATE EXTENSION ip4r;"
    pg_restore -U ${POSTGRES_USER} -h ${POSTGRES_HOST} -w -j 4 -d ${POSTGRES_DB} /tmp/${POSTGRES_DB}.psql
    rm -v /tmp/${POSTGRES_DB}.psql

La procédure correspond à ce qui a déjà été décrit ci-dessus. Le seul changement est la suppression du fichier psql après que tout le travail a été ajouté.

Remarque : à la fois dans le script de récupération et dans le script de désinstallation, la base de données est supprimée à chaque fois. Ceci est fait pour éviter d'éventuels conflits lors de la recréation de la revue: vous devez vous assurer que la base de données est réellement supprimée. En outre, ce problème peut potentiellement être résolu en ajoutant un indicateur --cleandans l'utilitaire pg_restore, mais soyez prudent: cet indicateur efface uniquement les données des éléments qui se trouvent dans le vidage lui-même, donc dans notre cas, cette option ne fonctionne pas.

En conséquence, nous avons obtenu un mécanisme de travail qui nécessite d'autres améliorations (jusqu'à remplacer les scripts Bash par du code plus élégant). Nous les laisserons en dehors du champ de l'article (bien que les commentaires sur le sujet, bien sûr, soient les bienvenus).

Mongodb


Le composant suivant est MongoDB. La principale difficulté est que pour ce SGBD, l'option de copier la base de données (comme dans PostgreSQL) existe plutôt nominalement, car:

  1. Il est dans un état déprécié .
  2. Selon les résultats de nos tests, nous n'avons pas trouvé de grande différence dans le temps de récupération de la base de données par rapport à l'habituel mongo_restore. Cependant, je note que les tests ont été effectués dans le cadre d'un projet - dans votre cas, les résultats peuvent être complètement différents.

Il s'avère que dans le cas d'un grand volume de base de données, un problème grave peut survenir: nous gagnons du temps sur la restauration de la base de données dans PgSQL, mais en même temps, nous restaurons le vidage dans Mongo pendant très longtemps. Au moment de la rédaction, et dans le cadre de l'infrastructure existante, nous avons vu trois façons (d'ailleurs, elles peuvent être combinées):

  1. La récupération peut prendre du temps, par exemple, si votre SGBD se trouve sur un système de fichiers réseau (pour les cas qui ne sont pas avec un environnement de production). Ensuite, vous pouvez simplement transférer le SGBD de l'étape vers un nœud distinct et utiliser le stockage local. Comme il ne s'agit pas de production, la vitesse de création d'une revue est plus critique pour nous.
  2. Vous pouvez prendre chaque récupération de travail dans un module distinct, ce qui vous permet de pré-exécuter les migrations et d'autres processus qui dépendent du fonctionnement du SGBD. Nous gagnons donc du temps en les complétant à l'avance.
  3. Parfois, vous pouvez réduire la taille du vidage en supprimant les données anciennes / non pertinentes - au point qu'il suffit de ne laisser que la structure de la base de données. Bien sûr, ce n'est pas pour les cas où un vidage complet est requis (par exemple, pour les tâches de test d'assurance qualité).

Si vous n'avez pas besoin de créer rapidement des environnements de révision, toutes les difficultés décrites peuvent être ignorées.

Nous, n'étant pas en mesure de copier la base de données de la même manière que PgSQL, irons dans le premier sens, c'est-à-dire récupération standard à partir d'une sauvegarde. L'algorithme est le même qu'avec PgSQL. C'est facile à voir si vous regardez les manifestes:

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: mongodb-scripts-on-delete
  annotations:
    "helm.sh/hook": "post-delete, pre-delete"
    "helm.sh/hook-weight": "1"
    "helm.sh/hook-delete-policy": before-hook-creation
data:
  review-delete-db.sh: |
    #!/bin/bash -x

    mongo ${MONGODB_NAME} --eval "db.dropDatabase()" --host ${MONGODB_REPLICASET}/${MONGODB_HOST}
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: mongodb-scripts
  annotations:
    "helm.sh/hook": "pre-install"
    "helm.sh/hook-weight": "1"
    "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
data:
  review-load-dump.sh: |
    #!/bin/bash -x

    curl --fail -vsL ${HOST_FORDEV}/latest_${MONGODB_NAME_STAGE}.gz -o /tmp/${MONGODB_NAME}.gz

    mongo ${MONGODB_NAME} --eval "db.dropDatabase()" --host ${MONGODB_REPLICASET}/${MONGODB_HOST}
    mongorestore --gzip --nsFrom "${MONGODB_NAME_STAGE}.*" --nsTo "${MONGODB_NAME}.*" --archive=/tmp/${MONGODB_NAME}.gz --host ${MONGODB_REPLICASET}/${MONGODB_HOST}

Il y a un détail important ici. Dans notre cas, MongoDB est dans le cluster et vous devez vous assurer que la connexion se produit toujours au nœud principal . Si vous spécifiez, par exemple, le premier hôte du quorum, il peut, après un certain temps, basculer du principal au secondaire, ce qui empêchera la création d'une base de données. Par conséquent, vous devez vous connecter non pas à un hôte, mais immédiatement à ReplicaSet , en répertoriant tous les hôtes qu'il contient . Pour cette seule raison, vous devez faire de MongoDB un StatefulSet afin que les noms d'hôte soient toujours les mêmes (sans mentionner que MongoDB est une application avec état par nature). Dans cette option, vous êtes assuré de vous connecter au nœud principal.

Pour MongoDB, nous supprimons également la base de données avant de créer la revue - cela se fait pour les mêmes raisons que dans PostgreSQL.

Dernière nuance: la base de données à réviser se trouvant dans le même environnement que la scène, un nom distinct est requis pour la base de données clonée. Si le vidage n'est pas un fichier BSON, l'erreur suivante se produit:

the --db and --collection args should only be used when restoring from a BSON file. Other uses are deprecated and will not exist in the future; use --nsInclude instead

Par conséquent, dans l'exemple ci-dessus, --nsFromet sont utilisés --nsTo.

Nous n'avons pas rencontré d'autres problèmes de récupération. Au final, j'ajouterai seulement que la documentation de copyDatabaseMongoDB est disponible ici - au cas où vous voudriez essayer cette option.

Rabbitmq


La dernière application sur notre liste d'exigences était RabbitMQ. C'est simple: vous devez créer un nouveau vhost au nom de l'utilisateur avec lequel l'application se connectera. Et puis supprimez-le.

Manifeste pour créer et supprimer des vhosts:

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: rabbitmq-configmap
  annotations:
    "helm.sh/hook": "pre-install"
    "helm.sh/hook-weight": "1"
    "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
data:
  rabbitmq-setup-vhost.sh: |
    #!/bin/bash -x

    /usr/local/bin/rabbitmqadmin -H ${RABBITMQ_HOST} -u ${RABBITMQ_USER} -p ${RABBITMQ_PASSWORD} declare vhost name=${RABBITMQ_VHOST}
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: rabbitmq-configmap-on-delete
  annotations:
    "helm.sh/hook": "post-delete, pre-delete"
    "helm.sh/hook-weight": "1"
    "helm.sh/hook-delete-policy": before-hook-creation
data:
  rabbitmq-delete-vhost.sh: |
    #!/bin/bash -x

    /usr/local/bin/rabbitmqadmin -H ${RABBITMQ_HOST} -u ${RABBITMQ_USER} -p ${RABBITMQ_PASSWORD} delete vhost name=${RABBITMQ_VHOST}

Avec de grosses difficultés dans RabbitMQ nous (jusqu'à présent?) N'avons pas rencontré. En général, la même approche peut s'appliquer à tous les autres services qui n'ont pas de lien critique pour les données.

désavantages


Pourquoi cette décision ne prétend-elle pas être des «meilleures pratiques»?

  1. Il se révèle un seul point de défaillance sous la forme d'un environnement scénique.
  2. Si une application dans un environnement de scène ne s'exécute que dans une seule réplique, nous devenons encore plus dépendants de l'hôte sur lequel cette application s'exécute. En conséquence, avec une augmentation du nombre d'environnements d'examen, la charge sur le nœud augmente proportionnellement sans la capacité d'équilibrer cette charge.

Il n'a pas été possible de résoudre complètement ces deux problèmes, compte tenu des capacités de l'infrastructure d'un projet particulier, cependant, les dommages potentiels peuvent être minimisés par le clustering (ajout de nouveaux nœuds) et la mise à l'échelle verticale.

Conclusion


Au fur et à mesure que l'application se développe et avec l'augmentation du nombre de développeurs, tôt ou tard, la charge sur les environnements de révision augmente et de nouvelles exigences leur sont ajoutées. Il est important que les développeurs apportent les prochains changements de production le plus rapidement possible, mais pour ce faire, nous avons besoin d'environnements de révision dynamiques qui rendent le développement «parallèle». En conséquence, la charge sur l'infrastructure augmente et le temps de création de tels environnements augmente.

Cet article a été écrit sur la base d'une expérience réelle et plutôt spécifique. Ce n'est que dans des cas exceptionnels que nous isolons des services dans des environnements statiques, et ici, il s'agissait spécifiquement de lui. Une telle mesure nécessaire nous a permis d'accélérer le développement et le débogage de l'application - grâce à la possibilité de créer rapidement des environnements de révision à partir de zéro.

Lorsque nous avons commencé à faire cette tâche, cela semblait très simple, mais en y travaillant, nous avons trouvé de nombreuses nuances. Ce sont eux qui ont été rassemblés dans l'article final: bien qu'ils ne soient pas universels, ils peuvent servir d'exemple pour la base / l'inspiration de leurs propres décisions sur l'accélération des environnements de révision.

PS


Lisez aussi dans notre blog:


All Articles