Suite à l’article de tferdinand sur l’installation de hugo sur un S3 (d’ailleurs vivement les prochaines parties), et surtout suite au commentaire de Lord, je me suis demandé s’il était possible de faire la même chose en on-premise (en fait j’avais déjà la réponse). Et vous savez quoi ? C’est possible.

Pour ceci nous utiliserons ceci :

  • gitea pour remplacer github
  • drone pour remplacer github actions
  • minio pour remplacer s3
  • traefik en reverse proxy

Nous installerons le tout sur docker, simplement parce que mon infra tourne actuellement dessus (en l’attente d’ansibiliser tout, mais ça prend beaucoup de temps).

Je pars donc du principe que vous avez déjà un traefik d’installé et configuré. Personnellement pour traefik, j’utilise des fichiers de configuration à plat, au lieu des labels, tout simplement pour avoir un docker-compose.yml beaucoup plus clair.

Actuellement mon CI/CD est géré par gitlab-ci, avec création d’une image docker puis déploiement sur monde serveur. Je vais donc faire une migration de mon blog vers cette nouvelle stack.

Gitea

Pour Gitea, nous utiliserons l’image officielle, avec une base de donnée postgreSQL.

Docker-compose

  gitea:
    image: gitea/gitea:1
    mem_limit: "500m"
    cpus: "0.2"
    environment:
      - USER_UID=1000
      - USER_GID=1000
    restart: always
    networks:
      - net
      - lan
    volumes:
      - ./data/gitea:/data
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro
    ports:
      - "222:22"
    depends_on:
      - gitea-db

  gitea-db:
    image: postgres:13
    mem_limit: "100m"
    cpus: "0.2"
    restart: always
    environment:
      - POSTGRES_USER=gitea
      - POSTGRES_PASSWORD=gitea
      - POSTGRES_DB=gitea
    networks:
      - lan
    volumes:
      - ./data/gitea-db:/var/lib/postgresql/data

Pensez à adapter les variables et les volumes

Puis on lance :

$ docker-compose up -d

Traefik

Nous créons donc un service et un router coté traefik :

http:
  services:
    gitea:
      loadBalancer:
        servers:
          - url: "http://gitea:3000"

  routers:
    gitea:
      rule: "Host(`gitea.domaine.fr`)"
      entryPoints:
        - "websecure"
      service: "gitea@file"
      tls: {}

Configurer gitea

Pour ceci, il suffit d’aller sur https://gitea.domaine.fr, et de suivre les instructions, pour configurer la base de donnée, votre utilisateur administrateur, et quelques options.
Une fois ceci fait, vous retrouverez toutes les options de votre instance dans ./data/gitea/gitea/conf/app.yml, vous pourrez modifier au besoin.

Drone

Gitea

Avant d’installer drone, il faudra créer une application Oauth sur gitea, pour ceci, vous pouvez suivre cette page de la documentation de Drone.
Mais en gros :

  1. Allez dans configuration
  2. Puis dans l’onglet Applications
  3. Dans l’encadrer “Gérer les applications OAuth2”, choisir un nom d’application comme drone puis ajouter l’url de redirection https://drone.domaine.fr/login
  4. Cliquez sur Créer une application
  5. Sur cette nouvelle page, copier l’ID du client et le secret, car il ne serons plus accessibleune fois Enrigistré.
  6. Pour finir, on clique sur enregistrer

Docker-compose

Tout comme gitea, nous utiliserons une base de donnée postgreSQL avec drone, une base sqlite peux vous être suffisant. Voici donc le docker-compose.yml :

  drone:
    image: drone/drone:2
    restart: always
    mem_limit: "500m"
    cpus: "0.2"
    environment:
      - DRONE_GITEA_SERVER=https://gitea.domaine.fr
      - DRONE_GITEA_CLIENT_ID=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXXX
      - DRONE_GITEA_CLIENT_SECRET=XXXXXXXXXXXXXXXXXXXXXXXXXXXX
      - DRONE_RPC_SECRET=XXXXXXXXXXXXXXXXXXXX
      - DRONE_SERVER_HOST=drone.domaine.fr
      - DRONE_SERVER_PROTO=https
      - DRONE_DATABASE_DRIVER=postgres
      - DRONE_DATABASE_DATASOURCE=postgres://drone:drone@drone-db:5432/drone?sslmode=disable
      - DRONE_GIT_ALWAYS_AUTH=true
    networks:
      - lan
      - net
    depends_on:
      - drone-db
  drone-db:
    image: postgres:13
    mem_limit: "100m"
    cpus: "0.2"
    restart: always
    environment:
      - POSTGRES_USER=drone
      - POSTGRES_PASSWORD=drone
      - POSTGRES_DB=drone
    networks:
      - lan
    volumes:
      - ./data/drone-db:/var/lib/postgresql/data

Pensez bien à remplacer les variables DRONE_GITEA_CLIENT_ID et DRONE_GITEA_CLIENT_SECRET obtenus par la création d’une application Oauth sur gitea.
Pour la variable DRONE_RPC_SECRET, c’est à vous de la générer, la doc officiel conseille cette commande : openssl rand -hex 16.

Traefik

Nous créons également un service et un router coté traefik :

http:
  services:
    drone:
      loadBalancer:
        servers:
          - url: "http://drone:80"

  routers:
    drone:
      rule: "Host(`drone.domaine.fr`)"
      entryPoints:
        - "websecure"
      service: "drone@file"
      tls: {}

Il ne vous reste plus qu’à vous rendre sur https://drone.domaine.fr, et vous authentifier via Gitea.

Installation d’un runner

Pour exécuter nos pipelines, nous avons besoin d’un runner. Ici nous utiliserons un runner docker, pour ceci voici comment le configurer :

  drone-runner:
    image: drone/drone-runner-docker:1
    mem_limit: "100m"
    cpus: "0.2"
    restart: always
    networks:
      - lan
      - net
    environment:
      - DRONE_RPC_PROTO=http
      - DRONE_RPC_HOST=drone:80
      - DRONE_RPC_SECRET=XXXXXXXXXXXXXXXXXXXXXXX # Doit être le même que sur drone
      - DRONE_RUNNER_CAPACITY=2
      - DRONE_RUNNER_NAME=runner
      - DRONE_RUNNER_NETWORKS=docker_net # Networks créer par docker-compose
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock

Minio

Maintenant que nous avons notre stack de CI/CD, nous allons installer Minio.
Minio est un serveur de fichier objet comme AWS S3, avec la même API, mais en auto-hébergé. Il est fortement scalable au besoin. Ici nous n’utiliserons qu’une seule instance.

Docker-compose

  minio:
    image: minio/minio
    mem_limit: '500m'
    cpus: '0.2'
    networks:
      - lan
      - net
    volumes:
      - ./data/minio:/data
    environment:
      - MINIO_ROOT_USER=admin
      - MINIO_ROOT_PASSWORD=unicornIsReal
    ports:
      - "9000:9000"
      - "9001:9001"
    command: server --console-address ":9001" --address ":9000" /data

Pas de création de configuration traefik cette fois-ci, n’ayant pas encore regardé la sécurité du côté minio, je préfère garder ceci en local pour le moment.

Création du bucket

Il nous faut un bucket pour stocker notre blog, nous commençons par créer un alias pour nous connecter à notre S3 :

$ docker run -ti --rm --network docker_lan -v /docker/data/mc:/root/.mc minio/mc alias set minio http://minio:9000 admin unicornIsReal --api s3v4

Puis nous pouvons créer un bucket :

docker run -ti --rm --network docker_lan -v /docker/data/mc:/root/.mc minio/mc mb minio/blog

Puis surtout, il faut autoriser à télécharger les fichiers sans authentification :

docker run -ti --rm --network docker_lan -v /docker/data/mc:/root/.mc minio/mc policy set download minio/blog

A partir de maintenant, vous pouvez déjà tester si ça fonctionne, en tapant directement sur http://<URL_DE_VOTRE_SERVEUR>:9000/blog, mais bon nous n’avons rien actuellement.

Pipeline

Activation du repo

D’abord, il faut activer le repo sur drone, il suffit de se rendre sur le dashboard de drone, de choisir le repository et de cliquer sur “Activate Repository”. Le repo est maintenant surveillé par drone.

Création de la pipeline

Dans notre repo git, nous allons créer un fichier .drone.yml, où nous aurons les instructions pour la construction et le déploiement de notre blog :

kind: pipeline
type: docker
name: blog

steps:
- name: build
  image: plugins/hugo
  settings:
    hugo_version: 0.91.2
    validate: true

- name: deploy
  image: plugins/s3
  settings:
    bucket: blog
    access_key: admin
    secret_key: unicornIsReal
    source: public/**/*
    target: /
    endpoint: http://minio:9000
    strip_prefix: public/
    path_style: true
  depends_on:
    - build

Là nous avons de la chance, il existe des plugins spécialement pour notre besoin, donc même pas besoin de scripter.

Nous pouvons donc pousser notre code avec un git push, et notre pipeline devrait automatiquement s’exécuter.
Une fois notre pipeline ok, nous pouvons tester via http://<URL_DE_VOTRE_SERVEUR>:9000/blog, vous devriez avoir la liste des fichiers présents dans votre bucket.

Vous pouvez également afficher directement votre page, via http://<URL_DE_VOTRE_SERVEUR>:9000/blog/index.html, cependant vous aller avoir quelques soucis avec l’affichage. Car tout simplement, il n’arrive pas à charger les librairies CSS et/ou JS. C’est là que traefik va rentrer en jeux.

Traefik

Pour traefik, cette fois-ci nous aurons quelques spécificités.

  • Suppression d’un préfixe entre le reverse et minio, effectivement, j’accède à mon blog via https://blog.domaine.fr, mais derrière nous avons http://minio:9000/blog.
  • Forcer la lecture du index.html, en effet, minio n’est pas capable de faire ceci, et de base ce n’est pas le rôle de traefik non plus mais nous pouvons tricher pour le faire. Si nous ne mettons pas ceci, il affichera toujours le contenu du répertoire.
  • Et au final, il faudra prévoir également une page d’erreur en cas de 404 par exemple, afin de ne pas générer une page d’erreur minio.

Donc voici ma configuration de traefik :

http:
  services:
    blog:
      loadBalancer:
        servers:
          - url: "http://minio:9000"


  middlewares:
    add-blog:
      addPrefix:
        prefix: "/blog"
    add-index:
      replacePathRegex:
        regex: "^(.*)/$"
        replacement: "$1/index.html"
    error:
      errors:
        status:
          - "400-499"
          - "500-599"
        service: "blog@file"
        query: "/blog/404.html"

  routers:
    blog:
      rule: "Host(`blog.domaine.fr`)"
      entryPoints:
        - "websecure"
      middlewares:
        - "add-blog@file"
        - "add-index@file"
        - "error@file"
      service: "blog@file"
      tls: {}

Nous avons donc 3 middlewares :

  • add-blog : De type addPrefix qui permet entre le router et le service de rajouter /blog. Grace à ceci, quand vous irez sur https://blog.domaine.fr/index.html, il tapera sur http://minio:9000/blog/index.html.
  • add-index : De type replacePathRegex qui permet l’ajout automatique du index.html si l’url se termine par /.
  • error: De type errors pour pouvoir afficher une page spécifique si une erreur entre 400 et 599. Pour l’instant j’ai mis qu’une 404.html pour tout.

Avec ceci, normalement vous avez un blog fonctionnel.

Amélioration

Utilisation de secret

Pour sécuriser un peu notre pipeline, nous pouvons ajouter des secrets pour la gestion des identifiants minio, ce que nous donne pour cette étape :

- name: deploy
  image: plugins/s3
  settings:
    bucket: blog
    access_key:
      from_secret: minio_access_key_id
    secret_key:
      from_secret: minio_access_key_secret
    source: public/**/*
    target: /
    endpoint: http://minio:9000
    strip_prefix: public/
    path_style: true
  depends_on:
    - build

Plusieurs environnements

Là j’ai déjà déployé sur ma production, mais je peux vouloir plusieurs environnements, dev, staging et production par exemple. Pour ceci il va falloir multiplier les buckets, revoir un peu la configuration de traefik, et ajouter des étapes au .drone.yml. Nous pourrions même imaginer des environnements à la volée, dès qu’on pousse sur une branche, nous créons un domaine, un bucket et une règle traefik pour ceci.

Mon .drone.yml final (ou presque)

Voilà mon .drone.yml actuel, il est fonctionnel mais pas encore optimal :

kind: pipeline
type: docker
name: blog

steps:
- name: build
  image: plugins/hugo
  settings:
    hugo_version: 0.91.2
    validate: true

- name: deploy dev
  image: plugins/s3
  settings:
    bucket: blog-dev
    access_key:
      from_secret: minio_access_key_id
    secret_key:
      from_secret: minio_access_key_secret
    source: public/**/*
    target: /
    endpoint: http://minio:9000
    strip_prefix: public/
    path_style: true
  when:
    branch:
      - dev
  depends_on:
    - build

- name: deploy staging
  image: plugins/s3
  settings:
    bucket: blog-staging
    access_key:
      from_secret: minio_access_key_id
    secret_key:
      from_secret: minio_access_key_secret
    source: public/**/*
    target: /
    endpoint: http://minio:9000
    strip_prefix: public/
    path_style: true
  when:
    branch:
      - staging
  depends_on:
    - build

- name: deploy production
  image: plugins/s3
  settings:
    bucket: blog
    access_key:
      from_secret: minio_access_key_id
    secret_key:
      from_secret: minio_access_key_secret
    source: public/**/*
    target: /
    endpoint: http://minio:9000
    strip_prefix: public/
    path_style: true
  when:
    branch:
      - main
  depends_on:
    - build

Il n’est pas encore parfait, car drone a beaucoup changé depuis ma dernière utilisation, et là j’ai tout de même un souci avec une altération de livrable entre les environnements, je verrais par la suite comment bien configurer tout ça.

Conclusion

Comme vous pouvez le voir, ça fonctionne bien … Ba oui, ce blog est maintenant hébergé sur Minio.
Je ne suis pas sûr que je vais garder cette installation, mais elle me plait bien, et j’ai adoré la mettre en place, ça change des installations plus classique.

Ressources