Chez Kinsta, nous avons des projets de toutes tailles pour l’hébergement d’applications, l’hébergement de bases de données et l’hébergement WordPress infogéré.

Avec les solutions d’hébergement cloud de Kinsta, vous pouvez déployer des applications dans un certain nombre de langages et de frameworks, tels que NodeJS, PHP, Ruby, Go, Scala et Python. Avec un fichier Docker, vous pouvez déployer n’importe quelle application. Vous pouvez connecter votre dépôt Git (hébergé sur GitHub, GitLab ou Bitbucket) pour déployer votre code directement sur Kinsta.

Vous pouvez héberger des bases de données MariaDB, Redis, MySQL et PostgreSQL prêtes à l’emploi, ce qui vous permet de vous concentrer sur le développement de vos applications plutôt que de vous préoccuper des configurations d’hébergement.

Et si vous choisissez notre hébergement WordPress infogéré, vous bénéficiez de la puissance des machines Google Cloud C2 sur leur réseau Premium et de la sécurité intégrée Cloudflare, ce qui fait de vos sites WordPress les plus rapides et les plus sûrs du marché.

Surmonter le défi du développement d’applications Cloud-Natives au sein d’une équipe distribuée

L’un des plus grands défis du développement et de la maintenance d’applications cloud-natives au niveau de l’entreprise est d’avoir une expérience cohérente tout au long du cycle de développement. C’est encore plus difficile pour les entreprises distantes avec des équipes distribuées travaillant sur différentes plateformes, avec des configurations différentes et une communication asynchrone. Nous devons fournir une solution cohérente, fiable et évolutive qui fonctionne pour :

  • Les développeurs et les équipes d’assurance qualité, quel que soit leur système d’exploitation, créent une configuration simple et minimale pour développer et tester des fonctionnalités.
  • Les équipes DevOps, SysOps et Infra, pour configurer et maintenir les environnements de staging et de production.

Chez Kinsta, nous nous appuyons fortement sur Docker pour cette expérience cohérente à chaque étape, du développement à la production. Dans cet article, nous vous guidons à travers :

  • Comment tirer parti de Docker Desktop pour accroître la productivité des développeurs.
  • Comment nous construisons des images Docker et les poussons vers Google Container Registry via des pipelines CI avec CircleCI et GitHub Actions.
  • Comment nous utilisons les pipelines de CI pour promouvoir des changements incrémentaux vers la production en utilisant des images Docker, Google Kubernetes Engine et Cloud Deploy.
  • Comment l’équipe QA utilise de manière transparente des images Docker pré-construites dans différents environnements.

Utilisation de Docker Desktop pour améliorer l’expérience des développeurs

L’exécution d’une application en local exige que les développeurs préparent méticuleusement l’environnement, installent toutes les dépendances, configurent les serveurs et les services et s’assurent qu’ils sont correctement configurés. Lorsque vous exécutez plusieurs applications, cela peut s’avérer fastidieux, en particulier lorsqu’il s’agit de projets complexes comportant de nombreuses dépendances. Lorsque vous ajoutez à cette variable plusieurs contributeurs disposant de plusieurs systèmes d’exploitation, le chaos s’installe. Pour l’éviter, nous utilisons Docker.

Avec Docker, vous pouvez déclarer les configurations de l’environnement, installer les dépendances et construire des images avec tout ce qu’il faut là où il faut. N’importe qui, n’importe où, avec n’importe quel système d’exploitation peut utiliser les mêmes images et avoir exactement la même expérience que les autres.

Déclarer votre configuration avec Docker Compose

Pour commencer, créez un fichier Docker Compose, docker-compose.yml. Il s’agit d’un fichier de configuration déclaratif écrit au format YAML qui indique à Docker l’état souhaité de votre application. Docker utilise ces informations pour configurer l’environnement de votre application.

Les fichiers Docker Compose sont très utiles lorsque plusieurs conteneurs sont en cours d’exécution et qu’il existe des dépendances entre les conteneurs.

Pour créer votre fichier docker-compose.yml:

  1. Commencez par choisir un fichier image comme base de notre application. Recherchez sur Docker Hub et essayez de trouver une image Docker qui contient déjà les dépendances de votre application. Veillez à utiliser une balise d’image spécifique pour éviter les erreurs. L’utilisation de la balise latest peut provoquer des erreurs imprévues dans votre application. Vous pouvez utiliser plusieurs images de base pour plusieurs dépendances. Par exemple, une pour PostgreSQL et une pour Redis.
  2. Utilisez volumes pour persister les données sur votre hôte si vous en avez besoin. La persistance des données sur la machine hôte vous permet d’éviter de perdre des données si les conteneurs Docker sont supprimés ou si vous devez les recréer.
  3. Utilisez networks pour isoler votre installation afin d’éviter les conflits de réseau avec l’hôte et les autres conteneurs. Cela permet également à vos conteneurs de se retrouver facilement et de communiquer entre eux.

En réunissant tous ces éléments, nous obtenons un site docker-compose.yml qui ressemble à ceci :

version: '3.8'services:
  db:
    image: postgres:14.7-alpine3.17
    hostname: mk_db
    restart: on-failure
    ports:
      - ${DB_PORT:-5432}:5432
    volumes:
      - db_data:/var/lib/postgresql/data
    environment:
      POSTGRES_USER: ${DB_USER:-user}
      POSTGRES_PASSWORD: ${DB_PASSWORD:-password}
      POSTGRES_DB: ${DB_NAME:-main}
    networks:
      - mk_network
  redis:
    image: redis:6.2.11-alpine3.17
    hostname: mk_redis
    restart: on-failure
    ports:
      - ${REDIS_PORT:-6379}:6379
    networks:
      - mk_network
      
volumes:
  db_data:

networks:
  mk_network:
    name: mk_network

Conteneuriser l’application

Construire une image Docker pour votre application

Tout d’abord, nous devons créer une image Docker à l’aide de Dockerfile, puis l’appeler à partir de docker-compose.yml.

Pour créer votre fichier Dockerfile:

  1. Commencez par choisir une image de base. Utilisez l’image de base la plus petite possible pour votre application. En général, les images alpines sont très minimales et ne contiennent pratiquement aucun paquet supplémentaire. Vous pouvez commencer par une image alpine et construire à partir de celle-ci :
    FROM node:18.15.0-alpine3.17
    
  2. Parfois, vous devez utiliser une architecture de processeur spécifique pour éviter les conflits. Par exemple, supposons que vous utilisiez un processeur arm64-based mais que vous deviez construire une image amd64. Vous pouvez le faire en spécifiant -- platform dans Dockerfile:
    FROM --platform=amd64 node:18.15.0-alpine3.17
    
  3. Définissez le répertoire d’application, installez les dépendances et copiez le résultat dans votre répertoire racine :
    WORKDIR /opt/app 
    COPY package.json yarn.lock ./ 
    RUN yarn install 
    COPY . .
  4. Appelez l’application Dockerfile à partir de docker-compose.yml:
    services:
      ...redis
      ...db
      
      app:
        build:
          context: .
          dockerfile: Dockerfile
        platforms:
          - "linux/amd64"
        command: yarn dev
        restart: on-failure
        ports:
          - ${PORT:-4000}:${PORT:-4000}
        networks:
          - mk_network
        depends_on:
          - redis
          - db
  5. Implémentez le chargement automatique de sorte que lorsque vous modifiez quelque chose dans le code source, vous puissiez prévisualiser vos changements immédiatement sans avoir à reconstruire l’application manuellement. Pour cela, construisez d’abord l’image, puis exécutez-la dans un service séparé :
    services:
      ... redis
      ... db
      
      build-docker:
        image: myapp
        build:
          context: .
          dockerfile: Dockerfile
      app:
        image: myapp
        platforms:
          - "linux/amd64"
        command: yarn dev
        restart: on-failure
        ports:
          - ${PORT:-4000}:${PORT:-4000}
        volumes:
          - .:/opt/app
          - node_modules:/opt/app/node_modules
        networks:
          - mk_network
        depends_on:
          - redis
          - db
          - build-docker
          
    volumes:
      node_modules:

Conseil de pro : Notez que node_modules est également monté explicitement pour éviter les problèmes de paquets spécifiques à la plate-forme. Cela signifie qu’au lieu d’utiliser le site node_modules sur l’hôte, le conteneur docker utilise le sien mais le mappe sur l’hôte dans un volume séparé.

Construire progressivement les images de production avec l’intégration continue

La majorité de nos applications et services utilisent CI/CD pour le déploiement. Docker joue un rôle important dans ce processus. Chaque changement dans la branche principale déclenche immédiatement un pipeline de construction via GitHub Actions ou CircleCI. Le flux de travail général est très simple : il installe les dépendances, exécute les tests, construit l’image Docker, et la pousse vers Google Container Registry (ou Artifact Registry). La partie dont nous parlons dans cet article est l’étape de construction.

Construction des images Docker

Nous utilisons des constructions en plusieurs étapes pour des raisons de sécurité et de performance.

Étape 1 : Construction

Dans cette étape, nous copions la base de code entière avec toutes les sources et la configuration, nous installons toutes les dépendances, y compris les dépendances de développement, et nous construisons l’application. Elle crée un dossier dist/ et y copie la version construite du code. Mais cette image est beaucoup trop volumineuse et comporte un grand nombre d’empreintes pour être utilisée en production. De plus, comme nous utilisons des registres NPM privés, nous utilisons également notre site privé NPM_TOKEN à ce stade. Nous ne voulons donc pas que cette étape soit exposée au monde extérieur. La seule chose dont nous avons besoin à ce stade est le dossier dist/.

Étape 2 : Production

La plupart des gens utilisent cette étape pour l’exécution car elle est très proche de ce dont nous avons besoin pour faire fonctionner l’application. Cependant, nous devons encore installer les dépendances de production, ce qui signifie que nous laissons des traces et que nous avons besoin du dossier NPM_TOKEN. Cette étape n’est donc pas encore prête à être exposée. Faites également attention à yarn cache clean sur la ligne 19. Cette petite commande réduit la taille de notre image de 60 %.

Étape 3 : Exécution

La dernière étape doit être aussi fine que possible avec un minimum d’empreinte. Nous nous contentons donc de copier l’application toute prête de la production et de passer à autre chose. Nous laissons tous ces trucs yarn et NPM_TOKEN derrière nous et n’exécutons que l’application.

C’est la version finale de Dockerfile.production:

# Stage 1: build the source code 
FROM node:18.15.0-alpine3.17 as builder 
WORKDIR /opt/app 
COPY package.json yarn.lock ./ 
RUN yarn install 
COPY . . 
RUN yarn build 

# Stage 2: copy the built version and build the production dependencies FROM node:18.15.0-alpine3.17 as production 
WORKDIR /opt/app 
COPY package.json yarn.lock ./ 
RUN yarn install --production && yarn cache clean 
COPY --from=builder /opt/app/dist/ ./dist/ 

# Stage 3: copy the production ready app to runtime 
FROM node:18.15.0-alpine3.17 as runtime 
WORKDIR /opt/app 
COPY --from=production /opt/app/ . 
CMD ["yarn", "start"]

Notez que, pour toutes les étapes, nous commençons par copier les fichiers package.json et yarn.lock, nous installons les dépendances, puis nous copions le reste de la base de code. La raison en est que Docker construit chaque commande comme une couche au-dessus de la précédente. Et chaque compilation peut utiliser les couches précédentes si elles sont disponibles et ne construire les nouvelles couches qu’à des fins de performance.

Supposons que vous ayez modifié quelque chose dans src/services/service1.ts sans toucher aux paquets. Cela signifie que les quatre premières couches de l’étape de construction sont intactes et peuvent être réutilisées. Cela rend le processus de construction incroyablement plus rapide.

Pousser l’application vers le registre de conteneurs de Google via les pipelines CircleCI

Il y a plusieurs façons de construire une image Docker dans les pipelines CircleCI. Dans notre cas, nous avons choisi d’utiliser circleci/gcp-gcr orbs:

executors:
  docker-executor:
    docker:
      - image: cimg/base:2023.03
orbs:
  gcp-gcr: circleci/[email protected]
jobs:
  ...
  deploy:
    description: Build & push image to Google Artifact Registry
    executor: docker-executor
    steps:
      ...
      - gcp-gcr/build-image:
          image: my-app
          dockerfile: Dockerfile.production
          tag: ${CIRCLE_SHA1:0:7},latest
      - gcp-gcr/push-image:
          image: my-app
          tag: ${CIRCLE_SHA1:0:7},latest

Une configuration minimale est nécessaire pour construire et pousser notre application, grâce à Docker.

Pousser l’application vers le Google Container Registry via les actions GitHub

Comme alternative à CircleCI, nous pouvons utiliser GitHub Actions pour déployer l’application en continu. Nous configurons gcloud et construisons et poussons l’image Docker vers gcr.io:

jobs:
  setup-build:
    name: Setup, Build
    runs-on: ubuntu-latest

    steps:
    - name: Checkout
      uses: actions/checkout@v3

    - name: Get Image Tag
      run: |
        echo "TAG=$(git rev-parse --short HEAD)" >> $GITHUB_ENV

    - uses: google-github-actions/setup-gcloud@master
      with:
        service_account_key: ${{ secrets.GCP_SA_KEY }}
        project_id: ${{ secrets.GCP_PROJECT_ID }}

    - run: |-
        gcloud --quiet auth configure-docker

    - name: Build
      run: |-
        docker build 
          --tag "gcr.io/${{ secrets.GCP_PROJECT_ID }}/my-app:$TAG" 
          --tag "gcr.io/${{ secrets.GCP_PROJECT_ID }}/my-app:latest" 
          .

    - name: Push
      run: |-
        docker push "gcr.io/${{ secrets.GCP_PROJECT_ID }}/my-app:$TAG"
        docker push "gcr.io/${{ secrets.GCP_PROJECT_ID }}/my-app:latest"

À chaque petite modification apportée à la branche principale, nous construisons et poussons une nouvelle image Docker vers le registre.

Déploiement de modifications sur le moteur Google Kubernetes à l’aide de Google Delivery Pipelines

Le fait de disposer d’images Docker prêtes à l’emploi pour chaque modification facilite également le déploiement en production ou le retour en arrière en cas de problème. Nous utilisons Google Kubernetes Engine pour gérer et servir nos applications et nous utilisons Google Cloud Deploy et Delivery Pipelines pour notre processus de déploiement continu.

Lorsque l’image Docker est construite après chaque petite modification (avec le pipeline CI illustré ci-dessus), nous allons plus loin et déployons la modification sur notre cluster de développement à l’aide de gcloud. Jetons un coup d’œil à cette étape dans le pipeline CircleCI :

- run:
    name: Create new release
    command: gcloud deploy releases create release-${CIRCLE_SHA1:0:7} --delivery-pipeline my-del-pipeline --region $REGION --annotations commitId=$CIRCLE_SHA1 --images my-app=gcr.io/${PROJECT_ID}/my-app:${CIRCLE_SHA1:0:7}

Cela déclenche un processus de libération pour déployer les changements dans notre cluster Kubernetes de développement. Après avoir testé et obtenu les approbations, nous promouvons le changement vers staging et ensuite vers production. Tout cela est possible parce que nous disposons d’une image Docker isolée pour chaque changement, qui contient presque tout ce dont elle a besoin. Il nous suffit d’indiquer au déploiement quelle balise utiliser.

Comment l’équipe d’assurance qualité bénéficie de ce processus

L’équipe d’assurance qualité a surtout besoin d’une version cloud de pré-production des applications à tester. Cependant, elle a parfois besoin d’exécuter localement une application préconstruite (avec toutes les dépendances) pour tester une certaine fonctionnalité. Dans ces cas, ils ne veulent pas ou n’ont pas besoin de passer par la douleur de cloner le projet entier, d’installer les paquets npm, de construire l’application, de faire face aux erreurs des développeurs, et de revoir tout le processus de développement pour que l’application soit opérationnelle. Maintenant que tout est déjà disponible en tant qu’image Docker sur Google Container Registry, tout ce dont ils ont besoin, c’est d’un service dans le fichier Docker compose :

services:
  ...redis
  ...db
  
  app:
    image: gcr.io/${PROJECT_ID}/my-app:latest
    restart: on-failure
    ports:
      - ${PORT:-4000}:${PORT:-4000}
    environment:
      - NODE_ENV=production
      - REDIS_URL=redis://redis:6379
      - DATABASE_URL=postgresql://${DB_USER:-user}:${DB_PASSWORD:-password}@db:5432/main
    networks:
      - mk_network
    depends_on:
      - redis
      - db

Grâce à ce service, ils peuvent lancer l’application sur leurs machines locales à l’aide de conteneurs Docker :

docker compose up

Il s’agit là d’un grand pas vers la simplification des processus de test. Même si l’assurance qualité décide de tester un tag spécifique de l’application, elle peut facilement changer le tag de l’image à la ligne 6 et réexécuter la commande Docker Composer. Même s’ils décident de comparer simultanément différentes versions de l’application, ils peuvent facilement y parvenir avec quelques ajustements. Le plus grand avantage est de tenir notre équipe d’assurance qualité à l’écart des défis des développeurs.

Avantages de l’utilisation de Docker

  • Il n’y a pratiquement pas d’empreintes de dépendances : Si vous décidez de mettre à jour la version de Redis ou de Postgres, il vous suffit de modifier une ligne et de ré-exécuter l’application. Il n’est pas nécessaire de modifier quoi que ce soit sur votre système. De plus, si vous avez deux applications qui ont toutes deux besoin de Redis (peut-être même avec des versions différentes), vous pouvez les faire tourner toutes les deux dans leur propre environnement isolé sans qu’il y ait de conflit entre elles.
  • Plusieurs instances de l’application : Il y a beaucoup de cas où nous avons besoin d’exécuter la même application avec une commande différente. Par exemple, l’initialisation de la base de données, l’exécution de tests, l’observation des changements dans la base de données, ou l’écoute de messages. Dans chacun de ces cas, puisque nous avons déjà l’image construite prête, nous ajoutons simplement un autre service au fichier Docker compose avec une commande différente, et nous avons terminé.
  • Un environnement de test plus facile : Le plus souvent, vous avez juste besoin d’exécuter l’application. Vous n’avez pas besoin du code, des paquets ou des connexions aux bases de données locales. Vous voulez seulement vous assurer que l’application fonctionne correctement ou vous avez besoin d’une instance en cours d’exécution comme service de backend pendant que vous travaillez sur votre propre projet. Cela peut également être le cas pour l’assurance qualité, les réviseurs de demandes de retrait, ou même les personnes chargées de l’interface utilisateur qui veulent s’assurer que leur conception a été correctement mise en œuvre. Notre configuration Docker leur permet de prendre les choses en main très facilement, sans avoir à se préoccuper de trop de problèmes techniques.

Cet article a été publié à l’origine sur Docker.

Amin Choroomi

Développeur de logiciels chez Kinsta. Passionné par Docker et Kubernetes, il se spécialise dans le développement d'applications et les pratiques DevOps.