ArgoCD – Révolutionnez la Création de VM avec l’Automatisation GitOps

Dans l’univers dynamique du DevOps et de l’infrastructure en tant que code (IaC), l’automatisation et la gestion efficace des environnements de développement sont devenues primordiales. Parmi les outils de pointe qui facilitent cette transformation digitale, ArgoCD émerge comme un acteur clé. Cet article se propose de détailler la mise en œuvre d’une « usine à VM » à visée bureautique. En illustrant comment une instance d’ArgoCD, une solution GitOps native pour Kubernetes, pourrait permettre la création et la maintenance de “VM” dans des environnements cloud ou sur site.

ArgoCD embrasse le concept GitOps, intégrant étroitement les pratiques de développement logiciel dans la gestion des infrastructures, permettant ainsi une automatisation poussée, une sécurité renforcée et une traçabilité des déploiements. En adoptant une approche déclarative, ArgoCD facilite la définition de l’état désiré de votre infrastructure directement dans des dépôts Git. Cette méthodologie assure que l’état réel de votre système est continuellement synchronisé avec celui défini dans votre code, réduisant ainsi les risques d’erreurs humaines et accélérant le cycle de vie de déploiement.

L’approche de cette « usine à VM » avec ArgoCD est particulièrement séduisante pour les équipes souhaitant optimiser leurs workflows, améliorer leur efficacité opérationnelle, et garantir la cohérence et la fiabilité de leurs environnements de développement et de production. À travers cet article, nous dévoilerons comment configurer et exploiter ArgoCD pour orchestrer la création et la gestion de pseudos VMs avec une mise en pratique via des images préconfigurées.

Prérequis

Avant de plonger dans l’installation d’ArgoCD, assurez-vous de satisfaire aux prérequis suivants :

  • Un cluster Kubernetes en cours d’exécution.
  • kubectl configuré pour communiquer avec votre cluster Kubernetes.
  • Accès à un compte avec des privilèges administratifs sur le cluster.

Étape 1 : Déploiement d’ArgoCD sur Kubernetes

Le moyen le plus simple de déployer ArgoCD est d’utiliser son manifeste YAML officiel. Exécutez la commande suivante pour appliquer ce manifeste :

kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml

Cette commande crée un nouvel espace de noms appelé argocd et déploie ArgoCD dans cet espace de noms.

Étape 2 : Accès à l’interface utilisateur d’ArgoCD

Vous devez créer un objet Ingress pour exposer le service argocd-server. Voici un exemple de fichier YAML pour un objet Ingress qui expose ArgoCD sur un sous-domaine (par exemple, argocd.mondomaine.com). Adaptez les valeurs selon votre configuration spécifique, notamment le domaine et, si nécessaire, les annotations spécifiques à votre Ingress Controller :

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: argocd-server-ingress
  namespace: argocd
spec:
  rules:
  - host: argocd.mondomaine.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: argocd-server
            port:
              number: 80
  # Si vous utilisez cert-manager pour la gestion des certificats SSL, ajoutez la section suivante :
  # tls:
  # - hosts:
  #   - argocd.mondomaine.com
  #   secretName: argocd-secret-tls

L’application ArgoCD sera alors accessible sur http://argocd.mondomaine.com

Étape 3 : Configuration d’ArgoCD pour Scruter un Dépôt Git

Prérequis

  • Avoir créé un dépôt Git sous gitlab ou github
  • Avoir configuré un compte d’accès au dépôt Git pour ArgoCD
  • Avoir indiqué ce dépôt et ce compte dans l’interface d’administration d’ArgoCD

ArgoCD brille par sa capacité à automatiser le déploiement et la synchronisation des applications Kubernetes en se basant sur l’état déclaré dans un dépôt Git. Dans cette section, nous intégrerons une configuration spécifique pour que ArgoCD surveille un dépôt Git contenant les configurations d’infrastructure, notamment les définitions de machines virtuelles (VM), et les déploie automatiquement dans un cluster Kubernetes.

Création d’un projet ArgoCD

Un projet ArgoCD, tel que défini dans l’objet AppProject, sert à grouper des applications et à définir des politiques de sécurité, telles que les dépôts Git autorisés, les destinations de déploiement, et les ressources Kubernetes permises.

Créez un fichier nommé infrastructure-project.yaml avec le contenu suivant pour définir le projet :

apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
  name: infrastructure
  namespace: argocd
spec:
  description: "Projet d'infrastructure"
  sourceRepos:
    - "*"
  sourceNamespaces:
    - "*"
  destinations:
    - namespace: "*"
      server: https://kubernetes.default.svc
  clusterResourceWhitelist:
    - group: "*"
      kind: "*"

Appliquez cette configuration dans votre cluster :

kubectl apply -f infrastructure-project.yaml

Définir l’application

Une application ArgoCD définit la source du code (dépôt Git, chemin, révision), la destination (cluster, namespace) et la politique de synchronisation. L’objet Application spécifie comment et où déployer les configurations d’infrastructure à partir du dépôt Git.

Créez un fichier nommé infrastructure-apps-application.yaml avec la configuration suivante :

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: infrastructure-apps
  namespace: argocd
spec:
  project: infrastructure
  source:
    repoURL: 'https://gitlab.mondomaine.com/kubernetes/k8s.git'
    path: argocd/apps
    targetRevision: HEAD
    directory:
      recurse: true
  destination:
    server: 'https://kubernetes.default.svc'
    namespace: infrastructure
  syncPolicy:
    automated:
      selfHeal: true
      prune: false

Cette configuration crée une application nommée infrastructure-apps qui surveille le chemin argocd/apps dans le dépôt spécifié. La politique de synchronisation automatique est activée, permettant à ArgoCD de rétablir automatiquement l’état désiré sans suppression des ressources non désirées (prune: false).

Appliquez cette configuration :

kubectl apply -f infrastructure-apps-application.yaml

Validation et Suivi

Après l’application des configurations, ArgoCD commencera à surveiller le dépôt Git spécifié. Toute modification apportée aux fichiers dans le chemin argocd/apps (et tous les sous-dossiers présents dans apps) déclenchera une synchronisation pour s’assurer que l’état de votre cluster correspond à celui défini dans le dépôt.

Vous pouvez vérifier le statut de l’application via l’interface utilisateur d’ArgoCD ou en utilisant la commande suivante :

argocd app get infrastructure-apps

Pour voir les logs de synchronisation ou les éventuelles erreurs, utilisez :

argocd app logs infrastructure-apps

Étape 4 : Déploiement de MariaDB avec ArgoCD

Afin de permettre à vos équipes d’accéder aux VMs, nous allons déployer guacamole dans votre environnement, une application de bureau à distance basée sur le web.

Le déploiement de Guacamole nécessite une base MariaDB ou PostgreSQL. Nous avons choisi mariadb dans cet exemple mais vous pouvez très bien appliquer la même logique pour déployer PostgreSQL.

Configuration de MariaDB

Définition de l’application MariaDB dans ArgoCD : Commencez par créer un fichier mariadb.yaml dans le dossier argocd/apps de votre dépôt Git, qui servira à déployer MariaDB. Cette configuration inclut la création d’un espace de noms mariadb et définit une application ArgoCD qui déploie MariaDB en utilisant un chart Helm.

Contenu de mariadb.yaml :

apiVersion: v1
kind: Namespace
metadata:
  name: mariadb
---
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: mariadb
  namespace: argocd
spec:
  project: infrastructure
  source:
    path: mariadb
    repoURL: https://gitlab.mondomaine.com/kubernetes/k8s.git
    targetRevision: HEAD
  destination:
    server: 'https://kubernetes.default.svc'
    namespace: mariadb
  syncPolicy:
    syncOptions:
      - CreateNamespace=true
    automated:
      selfHeal: true
      prune: true
      allowEmpty: true

Chart et valeurs pour MariaDB : Dans un nouveau dossier mariadb à la racine de votre dépôt git, créez deux fichiers, Chart.yaml et values.yaml, pour spécifier le chart Helm de MariaDB et les paramètres de configuration désirés, notamment le mot de passe root, le nom de la base de données, l’utilisateur et le mot de passe.

Chart.yaml

Ce fichier définit les métadonnées du chart Helm que vous créez pour le déploiement de MariaDB. Il spécifie le nom du chart, sa version, et les dépendances, notamment le chart MariaDB officiel à utiliser.

apiVersion: v2
name: mariadb
version: 1.0.0
description: "A Helm chart for Kubernetes"

# Dépendances : Spécifie le chart MariaDB et la version souhaitée.
dependencies:
  - name: mariadb
    version: "18.*"
    repository: "oci://registry-1.docker.io/bitnamicharts"

Dans cet exemple, le chart mariadb dépend du chart MariaDB disponible dans le registre Bitnami. La version « 18.* » indique que n’importe quelle version majeure 18 sera sélectionnée. Vous devez vous assurer que la version spécifiée est compatible avec votre déploiement.

values.yaml

Ce fichier contient les configurations spécifiques pour le déploiement de MariaDB, telles que les paramètres de persistance, la configuration de l’authentification, et d’autres options spécifiques à l’application.

# Paramètres spécifiques à MariaDB
mariadb:
  primary:
    persistence:
      enabled: true               # Active la persistance pour garantir que les données survivent au redémarrage des pods
      storageClass: longhorn      # Utilise une classe de stockage spécifique pour le volume persistant
      accessMode: ReadWriteOnce   # Le volume peut être monté en écriture par un seul nœud
      size: 8Gi                   # Définit la taille du volume de stockage

  auth:
    rootPassword: "root"          # Mot de passe root pour l'accès à la base de données
    database: "guacamole"         # Nom de la base de données à créer automatiquement
    username: "guacamole"         # Nom de l'utilisateur de la base de données
    password: "password"          # Mot de passe pour l'utilisateur spécifié

Les paramètres sous mariadb.primary.persistence permettent de configurer la persistance des données. C’est crucial pour un service de base de données, car vous voudriez éviter de perdre vos données lorsque le pod de la base de données est redéployé ou supprimé.

Sous mariadb.auth, vous spécifiez les informations d’authentification pour la base de données, y compris le mot de passe root, le nom de la base de données par défaut à créer (dans ce cas, pour Guacamole), et les identifiants pour un utilisateur spécifique de cette base de données.

Lorsque vous poussez ces changements vers votre dépôt Git surveillé par ArgoCD, il détectera les changements et procédera au déploiement de MariaDB en utilisant les configurations spécifiées. ArgoCD facilite la gestion des déploiements en assurant que l’état de votre cluster Kubernetes correspond à l’état déclaré dans votre dépôt Git, permettant ainsi un déploiement et une mise à jour continus et automatisés de vos applications et services.

Étape 5 : Déploiement de guacamole avec ArgoCD

Pour le déploiement de Guacamole, créez un fichier guacamole.yaml dans le même dossier argocd/apps de votre dépôt Git. Ce fichier définit les ressources nécessaires pour déployer Guacamole, y compris le service Guacd et le service web Guacamole lui-même, ainsi qu’une configuration pour initialiser la base de données MariaDB.

# Création de l'espace de noms pour Guacamole
apiVersion: v1
kind: Namespace
metadata:
  name: guacamole
---
# Déploiement du service guacd
apiVersion: apps/v1
kind: Deployment
metadata:
  name: guacd
  namespace: guacamole
spec:
  selector:
    matchLabels:
      app: guacd
  template:
    metadata:
      labels:
        app: guacd
    spec:
      containers:
      - name: guacd
        image: guacamole/guacd
        ports:
        - containerPort: 4822
        # Probes pour surveiller l'état du service guacd
        livenessProbe:
          tcpSocket:
            port: 4822
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          tcpSocket:
            port: 4822
          initialDelaySeconds: 30
          periodSeconds: 10
---
# Déploiement de l'application web Guacamole
apiVersion: apps/v1
kind: Deployment
metadata:
  name: guacamole
  namespace: guacamole
spec:
  selector:
    matchLabels:
      app: guacamole
  template:
    metadata:
      labels:
        app: guacamole
    spec:
      containers:
      - name: guacamole
        image: guacamole/guacamole
        # Configuration de l'environnement pour se connecter à guacd et à la base de données
        env:
        - name: GUACD_HOSTNAME
          value: guacd.guacamole.svc.cluster.local
        - name: MYSQL_HOSTNAME
          value: mariadb.mariadb.svc.cluster.local
        - name: MYSQL_DATABASE
          value: guacamole
        - name: MYSQL_USER
          value: guacamole
        - name: MYSQL_PASSWORD
          value: password
        ports:
        - containerPort: 8080
        # Probes pour surveiller l'état de l'application web
        livenessProbe:
          httpGet:
            path: /
            port: 8080
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /
            port: 8080
          initialDelaySeconds: 30
          periodSeconds: 10
---
# Service pour exposer guacd
apiVersion: v1
kind: Service
metadata:
  name: guacd
  namespace: guacamole
spec:
  ports:
  - port: 4822
    targetPort: 4822
  selector:
    app: guacd
---
# Service pour exposer l'application web Guacamole
apiVersion: v1
kind: Service
metadata:
  name: guacamole
  namespace: guacamole
spec:
  ports:
  - port: 8080
    targetPort: 8080
  selector:
    app: guacamole
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: guacamole
  namespace: guacamole
spec:
  rules:
    - host: guacamole.mondomaine.com
      http:
        paths:
          - backend:
              service:
                name: guacamole
                port:
                  number: 8080
            path: /
            pathType: Prefix
  tls:
    - hosts:
        - guacamole.mondomaine.com
---
# ConfigMap contenant le script d'initialisation de la base de données
apiVersion: v1
kind: ConfigMap
metadata:
  name: guacamole-db-init-script
  namespace: guacamole
data:
  init.sql: |
    CREATE TABLE IF NOT EXISTS `guacamole_connection_group` ( `connection_group_id`   int(11)      NOT NULL AUTO_INCREMENT, `parent_id`             int(11), `connection_group_name` varchar(128) NOT NULL, `type`                  enum('ORGANIZATIONAL', 'BALANCING') NOT NULL DEFAULT 'ORGANIZATIONAL', `max_connections`          int(11), `max_connections_per_user` int(11), `enable_session_affinity`  boolean NOT NULL DEFAULT 0, PRIMARY KEY (`connection_group_id`), UNIQUE KEY `connection_group_name_parent` (`connection_group_name`, `parent_id`), CONSTRAINT `guacamole_connection_group_ibfk_1` FOREIGN KEY (`parent_id`) REFERENCES `guacamole_connection_group` (`connection_group_id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE IF NOT EXISTS `guacamole_connection` ( `connection_id`       int(11)      NOT NULL AUTO_INCREMENT, `connection_name`     varchar(128) NOT NULL, `parent_id`           int(11), `protocol`            varchar(32)  NOT NULL, `proxy_port`              integer, `proxy_hostname`          varchar(512), `proxy_encryption_method` enum('NONE', 'SSL'), `max_connections`          int(11), `max_connections_per_user` int(11), `connection_weight`        int(11), `failover_only`            boolean NOT NULL DEFAULT 0, PRIMARY KEY (`connection_id`), UNIQUE KEY `connection_name_parent` (`connection_name`, `parent_id`), CONSTRAINT `guacamole_connection_ibfk_1` FOREIGN KEY (`parent_id`) REFERENCES `guacamole_connection_group` (`connection_group_id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE IF NOT EXISTS `guacamole_entity` ( `entity_id`     int(11)            NOT NULL AUTO_INCREMENT, `name`          varchar(128)       NOT NULL, `type`          enum('USER', 'USER_GROUP') NOT NULL, PRIMARY KEY (`entity_id`), UNIQUE KEY `guacamole_entity_name_scope` (`type`, `name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE IF NOT EXISTS `guacamole_user` ( `user_id`       int(11)      NOT NULL AUTO_INCREMENT, `entity_id`     int(11)      NOT NULL, `password_hash` binary(32)   NOT NULL, `password_salt` binary(32), `password_date` datetime     NOT NULL, `disabled`      boolean      NOT NULL DEFAULT 0, `expired`       boolean      NOT NULL DEFAULT 0, `access_window_start`    TIME, `access_window_end`      TIME, `valid_from`  DATE, `valid_until` DATE, `timezone` VARCHAR(64), `full_name`           VARCHAR(256), `email_address`       VARCHAR(256), `organization`        VARCHAR(256), `organizational_role` VARCHAR(256), PRIMARY KEY (`user_id`), UNIQUE KEY `guacamole_user_single_entity` (`entity_id`), CONSTRAINT `guacamole_user_entity` FOREIGN KEY (`entity_id`) REFERENCES `guacamole_entity` (`entity_id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE IF NOT EXISTS `guacamole_user_group` ( `user_group_id` int(11)      NOT NULL AUTO_INCREMENT, `entity_id`     int(11)      NOT NULL, `disabled`      boolean      NOT NULL DEFAULT 0, PRIMARY KEY (`user_group_id`), UNIQUE KEY `guacamole_user_group_single_entity` (`entity_id`), CONSTRAINT `guacamole_user_group_entity` FOREIGN KEY (`entity_id`) REFERENCES `guacamole_entity` (`entity_id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE IF NOT EXISTS `guacamole_user_group_member` ( `user_group_id`    int(11)     NOT NULL, `member_entity_id` int(11)     NOT NULL, PRIMARY KEY (`user_group_id`, `member_entity_id`), CONSTRAINT `guacamole_user_group_member_parent_id` FOREIGN KEY (`user_group_id`) REFERENCES `guacamole_user_group` (`user_group_id`) ON DELETE CASCADE, CONSTRAINT `guacamole_user_group_member_entity_id` FOREIGN KEY (`member_entity_id`) REFERENCES `guacamole_entity` (`entity_id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE IF NOT EXISTS guacamole_sharing_profile ( `sharing_profile_id`    int(11)      NOT NULL AUTO_INCREMENT, `sharing_profile_name`  varchar(128) NOT NULL, `primary_connection_id` int(11)      NOT NULL, PRIMARY KEY (`sharing_profile_id`), UNIQUE KEY `sharing_profile_name_primary` (sharing_profile_name, primary_connection_id), CONSTRAINT `guacamole_sharing_profile_ibfk_1` FOREIGN KEY (`primary_connection_id`) REFERENCES `guacamole_connection` (`connection_id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE IF NOT EXISTS `guacamole_connection_parameter` ( `connection_id`   int(11)       NOT NULL, `parameter_name`  varchar(128)  NOT NULL, `parameter_value` varchar(4096) NOT NULL, PRIMARY KEY (`connection_id`,`parameter_name`), CONSTRAINT `guacamole_connection_parameter_ibfk_1` FOREIGN KEY (`connection_id`) REFERENCES `guacamole_connection` (`connection_id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE IF NOT EXISTS guacamole_sharing_profile_parameter ( `sharing_profile_id` integer       NOT NULL, `parameter_name`     varchar(128)  NOT NULL, `parameter_value`    varchar(4096) NOT NULL, PRIMARY KEY (`sharing_profile_id`, `parameter_name`), CONSTRAINT `guacamole_sharing_profile_parameter_ibfk_1` FOREIGN KEY (`sharing_profile_id`) REFERENCES `guacamole_sharing_profile` (`sharing_profile_id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE IF NOT EXISTS guacamole_user_attribute ( `user_id`         int(11)       NOT NULL, `attribute_name`  varchar(128)  NOT NULL, `attribute_value` varchar(4096) NOT NULL, PRIMARY KEY (user_id, attribute_name), KEY `user_id` (`user_id`), CONSTRAINT guacamole_user_attribute_ibfk_1 FOREIGN KEY (user_id) REFERENCES guacamole_user (user_id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE IF NOT EXISTS guacamole_user_group_attribute ( `user_group_id`   int(11)       NOT NULL, `attribute_name`  varchar(128)  NOT NULL, `attribute_value` varchar(4096) NOT NULL, PRIMARY KEY (`user_group_id`, `attribute_name`), KEY `user_group_id` (`user_group_id`), CONSTRAINT `guacamole_user_group_attribute_ibfk_1` FOREIGN KEY (`user_group_id`) REFERENCES `guacamole_user_group` (`user_group_id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE IF NOT EXISTS guacamole_connection_attribute ( `connection_id`   int(11)       NOT NULL, `attribute_name`  varchar(128)  NOT NULL, `attribute_value` varchar(4096) NOT NULL, PRIMARY KEY (connection_id, attribute_name), KEY `connection_id` (`connection_id`), CONSTRAINT guacamole_connection_attribute_ibfk_1 FOREIGN KEY (connection_id) REFERENCES guacamole_connection (connection_id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE IF NOT EXISTS guacamole_connection_group_attribute ( `connection_group_id` int(11)       NOT NULL, `attribute_name`      varchar(128)  NOT NULL, `attribute_value`     varchar(4096) NOT NULL, PRIMARY KEY (connection_group_id, attribute_name), KEY `connection_group_id` (`connection_group_id`), CONSTRAINT guacamole_connection_group_attribute_ibfk_1 FOREIGN KEY (connection_group_id) REFERENCES guacamole_connection_group (connection_group_id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE IF NOT EXISTS guacamole_sharing_profile_attribute ( `sharing_profile_id` int(11)       NOT NULL, `attribute_name`     varchar(128)  NOT NULL, `attribute_value`    varchar(4096) NOT NULL, PRIMARY KEY (sharing_profile_id, attribute_name), KEY `sharing_profile_id` (`sharing_profile_id`), CONSTRAINT guacamole_sharing_profile_attribute_ibfk_1 FOREIGN KEY (sharing_profile_id) REFERENCES guacamole_sharing_profile (sharing_profile_id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE IF NOT EXISTS `guacamole_connection_permission` ( `entity_id`     int(11) NOT NULL, `connection_id` int(11) NOT NULL, `permission`    enum('READ', 'UPDATE', 'DELETE', 'ADMINISTER') NOT NULL, PRIMARY KEY (`entity_id`,`connection_id`,`permission`), CONSTRAINT `guacamole_connection_permission_ibfk_1` FOREIGN KEY (`connection_id`) REFERENCES `guacamole_connection` (`connection_id`) ON DELETE CASCADE, CONSTRAINT `guacamole_connection_permission_entity` FOREIGN KEY (`entity_id`) REFERENCES `guacamole_entity` (`entity_id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE IF NOT EXISTS `guacamole_connection_group_permission` ( `entity_id`           int(11) NOT NULL, `connection_group_id` int(11) NOT NULL, `permission`          enum('READ', 'UPDATE', 'DELETE', 'ADMINISTER') NOT NULL, PRIMARY KEY (`entity_id`,`connection_group_id`,`permission`), CONSTRAINT `guacamole_connection_group_permission_ibfk_1` FOREIGN KEY (`connection_group_id`) REFERENCES `guacamole_connection_group` (`connection_group_id`) ON DELETE CASCADE, CONSTRAINT `guacamole_connection_group_permission_entity` FOREIGN KEY (`entity_id`) REFERENCES `guacamole_entity` (`entity_id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE IF NOT EXISTS guacamole_sharing_profile_permission ( `entity_id`          integer NOT NULL, `sharing_profile_id` integer NOT NULL, `permission`         enum('READ', 'UPDATE', 'DELETE', 'ADMINISTER') NOT NULL, PRIMARY KEY (`entity_id`, `sharing_profile_id`, `permission`), CONSTRAINT `guacamole_sharing_profile_permission_ibfk_1` FOREIGN KEY (`sharing_profile_id`) REFERENCES `guacamole_sharing_profile` (`sharing_profile_id`) ON DELETE CASCADE, CONSTRAINT `guacamole_sharing_profile_permission_entity` FOREIGN KEY (`entity_id`) REFERENCES `guacamole_entity` (`entity_id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE IF NOT EXISTS `guacamole_system_permission` ( `entity_id`  int(11) NOT NULL, `permission` enum('CREATE_CONNECTION', 'CREATE_CONNECTION_GROUP', 'CREATE_SHARING_PROFILE', 'CREATE_USER', 'CREATE_USER_GROUP', 'ADMINISTER') NOT NULL, PRIMARY KEY (`entity_id`,`permission`), CONSTRAINT `guacamole_system_permission_entity` FOREIGN KEY (`entity_id`) REFERENCES `guacamole_entity` (`entity_id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE IF NOT EXISTS `guacamole_user_permission` ( `entity_id`        int(11) NOT NULL, `affected_user_id` int(11) NOT NULL, `permission`       enum('READ', 'UPDATE', 'DELETE', 'ADMINISTER') NOT NULL, PRIMARY KEY (`entity_id`,`affected_user_id`,`permission`), CONSTRAINT `guacamole_user_permission_ibfk_1` FOREIGN KEY (`affected_user_id`) REFERENCES `guacamole_user` (`user_id`) ON DELETE CASCADE, CONSTRAINT `guacamole_user_permission_entity` FOREIGN KEY (`entity_id`) REFERENCES `guacamole_entity` (`entity_id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE IF NOT EXISTS `guacamole_user_group_permission` ( `entity_id`              int(11) NOT NULL, `affected_user_group_id` int(11) NOT NULL, `permission`             enum('READ', 'UPDATE', 'DELETE', 'ADMINISTER') NOT NULL, PRIMARY KEY (`entity_id`, `affected_user_group_id`, `permission`), CONSTRAINT `guacamole_user_group_permission_affected_user_group` FOREIGN KEY (`affected_user_group_id`) REFERENCES `guacamole_user_group` (`user_group_id`) ON DELETE CASCADE, CONSTRAINT `guacamole_user_group_permission_entity` FOREIGN KEY (`entity_id`) REFERENCES `guacamole_entity` (`entity_id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE IF NOT EXISTS `guacamole_connection_history` ( `history_id`           int(11)      NOT NULL AUTO_INCREMENT, `user_id`              int(11)      DEFAULT NULL, `username`             varchar(128) NOT NULL, `remote_host`          varchar(256) DEFAULT NULL, `connection_id`        int(11)      DEFAULT NULL, `connection_name`      varchar(128) NOT NULL, `sharing_profile_id`   int(11)      DEFAULT NULL, `sharing_profile_name` varchar(128) DEFAULT NULL, `start_date`           datetime     NOT NULL, `end_date`             datetime     DEFAULT NULL, PRIMARY KEY (`history_id`), KEY `user_id` (`user_id`), KEY `connection_id` (`connection_id`), KEY `sharing_profile_id` (`sharing_profile_id`), KEY `start_date` (`start_date`), KEY `end_date` (`end_date`), KEY `connection_start_date` (`connection_id`, `start_date`), CONSTRAINT `guacamole_connection_history_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `guacamole_user` (`user_id`) ON DELETE SET NULL, CONSTRAINT `guacamole_connection_history_ibfk_2` FOREIGN KEY (`connection_id`) REFERENCES `guacamole_connection` (`connection_id`) ON DELETE SET NULL, CONSTRAINT `guacamole_connection_history_ibfk_3` FOREIGN KEY (`sharing_profile_id`) REFERENCES `guacamole_sharing_profile` (`sharing_profile_id`) ON DELETE SET NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE IF NOT EXISTS guacamole_user_history ( `history_id`           int(11)      NOT NULL AUTO_INCREMENT, `user_id`              int(11)      DEFAULT NULL, `username`             varchar(128) NOT NULL, `remote_host`          varchar(256) DEFAULT NULL, `start_date`           datetime     NOT NULL, `end_date`             datetime     DEFAULT NULL, PRIMARY KEY (history_id), KEY `user_id` (`user_id`), KEY `start_date` (`start_date`), KEY `end_date` (`end_date`), KEY `user_start_date` (`user_id`, `start_date`), CONSTRAINT guacamole_user_history_ibfk_1 FOREIGN KEY (user_id) REFERENCES guacamole_user (user_id) ON DELETE SET NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE IF NOT EXISTS guacamole_user_password_history ( `password_history_id` int(11) NOT NULL AUTO_INCREMENT, `user_id`             int(11) NOT NULL, `password_hash` binary(32) NOT NULL, `password_salt` binary(32), `password_date` datetime   NOT NULL, PRIMARY KEY (`password_history_id`), KEY `user_id` (`user_id`), CONSTRAINT `guacamole_user_password_history_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `guacamole_user` (`user_id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; INSERT INTO guacamole_entity (name, type) VALUES ('guacadmin', 'USER') ON DUPLICATE KEY UPDATE name = VALUES(name), type = VALUES(type); INSERT IGNORE INTO guacamole_user (entity_id, password_hash, password_salt, password_date) SELECT entity_id, x'CA458A7D494E3BE824F5E1E175A1556C0F8EEF2C2D7DF3633BEC4A29C4411960', x'FE24ADC5E11E2B25288D1704ABE67A79E342ECC26064CE69C5B3177795A82264', NOW() FROM guacamole_entity WHERE name = 'guacadmin'; INSERT IGNORE INTO guacamole_system_permission (entity_id, permission) SELECT entity_id, permission FROM ( SELECT 'guacadmin'  AS username, 'CREATE_CONNECTION'       AS permission UNION SELECT 'guacadmin'  AS username, 'CREATE_CONNECTION_GROUP' AS permission UNION SELECT 'guacadmin'  AS username, 'CREATE_SHARING_PROFILE'  AS permission UNION SELECT 'guacadmin'  AS username, 'CREATE_USER' AS permission UNION SELECT 'guacadmin'  AS username, 'CREATE_USER_GROUP' AS permission UNION SELECT 'guacadmin'  AS username, 'ADMINISTER' AS permission ) permissions JOIN guacamole_entity ON permissions.username = guacamole_entity.name AND guacamole_entity.type = 'USER'; INSERT IGNORE INTO guacamole_user_permission (entity_id, affected_user_id, permission) SELECT guacamole_entity.entity_id, guacamole_user.user_id, permission FROM ( SELECT 'guacadmin' AS username, 'guacadmin' AS affected_username, 'READ' AS permission UNION SELECT 'guacadmin' AS username, 'guacadmin' AS affected_username, 'UPDATE' AS permission UNION SELECT 'guacadmin' AS username, 'guacadmin' AS affected_username, 'ADMINISTER' AS permission ) permissions JOIN guacamole_entity ON permissions.username = guacamole_entity.name AND guacamole_entity.type = 'USER' JOIN guacamole_entity affected ON permissions.affected_username = affected.name AND guacamole_entity.type = 'USER' JOIN guacamole_user ON guacamole_user.entity_id = affected.entity_id;
---
# Job pour exécuter le script d'initialisation de la base de données
apiVersion: batch/v1
kind: Job
metadata:
  name: guacamole-db-init
  namespace: guacamole
spec:
  template:
    spec:
      containers:
      - name: init-db
        image: mysql:5.7
        command: ["sh", "-c", "mysql -u guacamole -p'password' -h mariadb.mariadb.svc.cluster.local -D guacamole < /mnt/init.sql"]
        volumeMounts:
        - name: script-volume
          mountPath: "/mnt"
      volumes:
      - name: script-volume
        configMap:
          name: guacamole-db-init-script
      restartPolicy: Never
---

Cela va déployer et configurer guacamole sur l’URL guacamole.mondomaine.com, l’utilisateur et mot de passe par défaut est guacadmin/guacadmin.

Étape 6 : Création des “VM” sous k8s

Je mets le mot « VM » entre guillemets car, concernant Linux, ce sont des containers et non pas des VM sous qemu ou kubevirt à proprement parlé.

VM Linux

On va ajouter un dossier VM dans le dépôt Git. Voici un exemple d’une VM générique sous alpine en utilisant l’image https://docs.linuxserver.io/images/docker-rdesktop/ qui est basée sur xrdp.

---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  finalizers:
    - kubernetes.io/pvc-protection
  name: alpine-xfce-pmietlicki
  namespace: guacamole
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 2Gi
  storageClassName: longhorn
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: alpine-xfce-pmietlicki
  namespace: guacamole
spec:
  strategy:
    rollingUpdate:
      maxSurge: 25%
      maxUnavailable: 25%
    type: RollingUpdate
  selector:
    matchLabels:
      workload.user.cattle.io/workloadselector: apps.deployment-guacamole-alpine-xfce-pmietlicki
  template:
    metadata:
      namespace: guacamole
      labels:
        workload.user.cattle.io/workloadselector: apps.deployment-guacamole-alpine-xfce-pmietlicki
    spec:
      containers:
        - env:
            - name: PUID
              value: '1000'
            - name: LANG
              value: 'fr_FR.UTF-8'
            - name: PGID
              value: '1000'
            - name: TZ
              value: 'Pacific/Noumea'
            - name: LANGUAGE
              value: 'fr_FR.UTF-8'
            - name: LC_ALL
              value: 'fr_FR.UTF-8'
            - name: HTTP_PROXY
              value: "http://proxy.mondomaine.com:3128"
            - name: HTTPS_PROXY
              value: "http://proxy.mondomaine.com:3128"
            - name: NO_PROXY
              value: "localhost, .svc.cluster.local"
          image: linuxserver/rdesktop:latest
          imagePullPolicy: Always
          livenessProbe:
            failureThreshold: 5
            periodSeconds: 60
            successThreshold: 1
            tcpSocket:
              port: 3389
            timeoutSeconds: 1
          name: alpine-xfce-pmietlicki
          ports:
            - containerPort: 3389
              name: rdp
              protocol: TCP
          readinessProbe:
            failureThreshold: 5
            periodSeconds: 60
            successThreshold: 1
            tcpSocket:
              port: 3389
            timeoutSeconds: 1
          resources:
            limits:
              memory: 2Gi
            requests:
              memory: 1Gi
          securityContext:
            allowPrivilegeEscalation: true
            capabilities: {}
            privileged: false
            readOnlyRootFilesystem: false
            runAsNonRoot: false
          startupProbe:
            failureThreshold: 5
            periodSeconds: 120
            successThreshold: 1
            tcpSocket:
              port: 3389
            timeoutSeconds: 1
          terminationMessagePath: /dev/termination-log
          terminationMessagePolicy: File
          volumeMounts:
            - mountPath: /config
              name: vol-home
            - mountPath: /dev/shm
              name: vol-shm
            - mountPath: /custom-cont-init.d
              name: vol-init
              readOnly: true
      restartPolicy: Always
      volumes:
        - name: vol-home
          persistentVolumeClaim:
            claimName: alpine-xfce-pmietlicki
        - emptyDir:
            medium: Memory
            sizeLimit: 2Gi
          name: vol-shm
        - configMap:
            defaultMode: 420
            name: custom-script-alpine-xfce-pmietlicki
            optional: false
          name: vol-init
---
apiVersion: v1
kind: Service
metadata:
  name: alpine-xfce-pmietlicki
  namespace: guacamole
spec:
  selector:
    workload.user.cattle.io/workloadselector: apps.deployment-guacamole-alpine-xfce-pmietlicki
  ports:
    - name: rdp
      port: 3389
      protocol: TCP
      targetPort: 3389
  sessionAffinity: None
  type: ClusterIP
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: custom-script-alpine-xfce-pmietlicki
  namespace: guacamole
data:
  install-script.sh: |+
    #!/bin/bash
    apk add --no-cache docker
    apk add --no-cache python3 python3-pip
    apk add --no-cache tmux
    apk add --no-cache git
---

ArgoCD va alors déployer automatiquement l’image et l’instancier. Le “custom script” vous permet d’ajouter les outils souhaités et vous garantit que l’image aura toujours ces outils déployés automatiquement.

L’intérêt de cette image est qu’elle est “jetable” c’est à dire qu’à chaque redéploiement du pod, l’instance est recréée de zéro, il n’y a que votre home utilisateur (dans /config) qui vous permettra de garder votre configuration et préférences applicatives.Cela est intéressant pour réduire une surface d’attaque éventuelle en cas de compromission.

Ajout dans guacamole

Une fois l’image déployée, il ne vous reste plus qu’à la configurer sous Guacamole en allant dans Paramètres/Connexion/Nouvelle Connexion :

Mettre le nom de l’hôte interne, dans notre exemple : alpine-xfce-pmietlicki.guacamole
Et le port 3389.

Dans la partie Authentification, l’identifiant de l’image est abc et le mot de passe abc. On peut très bien ajouter un utilisateur pour indiquer un autre identifiant et mot de passe sachant que l’image n’est accessible que depuis le NS guacamole donc que depuis l’interface guacamole qui est sécurisable par AD, LDAP ou TOTP.

On coche “ignorer le certificat du serveur” puisqu’il est autosigné.

Vous pouvez aussi définir les paramètres de base :

VM Windows

L’utilisation d’images Docker pour émuler des systèmes d’exploitation complets comme Windows dans un conteneur nécessite KVM (Kernel-based Virtual Machine) qui est une technologie de virtualisation intégrée au noyau Linux permettant d’exécuter des machines virtuelles (VM) avec des performances proches de celles d’un système natif, utilisant l’accélération matérielle fournie par les extensions de virtualisation du processeur comme Intel VT-x ou AMD-V.

Une image très intéressante pour déployer Windows est celle disponible sur : https://github.com/dockur/windows

Cette image vous permet d’installer différentes versions de Windows : Windows 11 Pro, Windows 10 Pro, Windows 10 LTSC, Windows 8.1 Pro, Windows 7 SP1, Windows Vista SP2, Windows XP SP3, Windows Server 2022, Windows Server 2019, Windows Server 2016, Windows Server 2012, Windows Server 2008, Tiny 11 Core, Tiny 11 et Tiny 10

Voici un exemple de configuration pour un Windows Lite (tiny11) que vous pouvez mettre dans le dossier VM :

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    workload.user.cattle.io/workloadselector: apps.deployment-guacamole-tiny11
  name: tiny11
  namespace: guacamole
spec:
  selector:
    matchLabels:
      workload.user.cattle.io/workloadselector: apps.deployment-guacamole-tiny11
  strategy:
    type: Recreate
  template:
      labels:
        workload.user.cattle.io/workloadselector: apps.deployment-guacamole-tiny11
      namespace: guacamole
    spec:
      containers:
        - env:
            - name: VERSION
              value: >-
                https://archive.org/download/tiny-11-NTDEV/tiny11%2023H2%20x64.iso
            - name: DISK_SIZE
              value: 100G
            - name: RAM_SIZE
              value: 5G
          image: dockurr/windows
          imagePullPolicy: Always
          name: tiny11
          ports:
            - containerPort: 3389
              name: rdp
              protocol: TCP
            - containerPort: 8006
              name: http
              protocol: TCP
          resources:
            limits:
              memory: 5Gi
          securityContext:
            allowPrivilegeEscalation: true
            capabilities:
              add:
                - NET_ADMIN
            privileged: true
          volumeMounts:
            - mountPath: /dev/kvm
              name: dev-kvm
            - mountPath: /storage
              name: vol-tiny11
      restartPolicy: Always
      volumes:
        - name: vol-tiny11
          persistentVolumeClaim:
            claimName: tiny11
        - hostPath:
            path: /dev/kvm
            type: CharDevice
          name: dev-kvm

A l’usage, j’ai remarqué qu’il vous faut activer le mode privileged (privileged: true) même en essayant d’activer la capacité “NET_ADMIN” seulement.

A noter que, même sur tiny11, 4Go de RAM et 2vcpu semble peu suffisant pour bien faire fonctionner Windows 11 pour un usage quotidien. Il vous faudra sans doute, à minima, 8Go de RAM de dispo pour utiliser pleinement le système.

La configuration sous guacamole est la même que pour Linux, la seule différence est que l’utilisateur par défaut est “docker” sans mot de passe.

Pour des versions spécifiques de Windows, il y a énormément d’ISO sur https://lecrabeinfo.net/

Activation du partage de fichiers sous guacamole

Pour activer le partage de fichier, vous devez utiliser un répertoire accessible en lecture/ecriture au tomcat de guacd. On peut, très simplement, utiliser /tmp.
Voici ce qu’il faudrait alors mettre dans la configuration :

Vous verrez alors un lecteur réseau dans l’explorateur, tous les fichiers que vous glisserez/déposerez via la fenêtre du navigateur dans laquelle se trouve votre connexion RDP guacamole se retrouveront à la racine de ce lecteur (et donc dans /tmp/).

Liste de l’ensemble des options de dockur/windows

VERSION="win11"          # Windows version
MANUAL="N"          # Automatic install
SAMBA="Y"          # Enable Samba share
DISK_IO="native"          # I/O Mode, can be set to 'native', 'threads' or 'io_uring'
DISK_FMT="raw"               # Disk file format, can be set to "raw" (default) or "qcow2"
DISK_FLAGS=""             # Specifies the options for use with the qcow2 disk format
DISK_CACHE="none"         # Caching mode, can be set to 'writeback' for better performance
DISK_DISCARD="on"         # Controls whether unmap (TRIM) commands are passed to the host.
DISK_ROTATION="1"         # Rotation rate, set to 1 for SSD storage and increase for HDD
GPU="N"         # GPU passthrough
VGA="virtio"    # VGA adaptor
DISPLAY="web"   # Display type
MAC=""          # MAC address
DHCP="N"          # Disable DHCP mode
NETWORK="Y"          # Enable networkcard
HOST_PORTS=""
VM_NET_DEV=""
VM_NET_TAP="qemu"
VM_NET_HOST="QEMU"
DNSMASQ_OPTS=""
DNSMASQ="/usr/sbin/dnsmasq"
DNSMASQ_CONF_DIR="/etc/dnsmasq.d"
HV="Y"          # Enable Hyper-V enlightments
KVM="Y"          # Enable KVM acceleration
CPU_FLAGS=""
CPU_MODEL="qemu64"
DEBUG="N"         # Disable debugging
CONSOLE="N"       # Disable console
MACHINE="q35"     # Machine selection
ALLOCATE="N"       # Preallocate diskspace
ARGUMENTS=""      # Extra QEMU parameters
CPU_CORES="1"     # Amount of CPU cores
RAM_SIZE="1G"     # Maximum RAM amount
DISK_SIZE="16G"            # Initial data disk size
BOOT_INDEX="10"            # Boot index of CD drive
TPM="Y"                  # Enable TPM
BOOT_MODE="windows"           # Boot mode
SERIAL="mon:stdio"
USB="qemu-xhci,id=xhci"
MONITOR="telnet:localhost:7100,server,nowait,nodelay"

Voir les options sur : https://github.com/qemus/qemu-docker

Bonus : création d’une “usine à VM” Linux

Comme la configuration yaml est toujours la même, j’ai voulu créer un code javascript pour générer le yaml pour chaque VM, pour chaque collaborateur. Vous pouvez évidemment l’adapter pour Windows ou d’autres types de VM.

Voici un début de code HTML/Javascript pour ce besoin :

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Generateur de Configuration VM Guacamole</title>
</head>
<body>
    <h2>Generateur de Configuration VM pour Guacamole</h2>
    <form id="configForm">
        <label for="version">Choisir la version:</label>
        <select name="version" id="version" required>
			<option value="alpine-xfce" selected>alpine-xfce</option>
            <option value="ubuntu-xfce">ubuntu-xfce</option>
            <option value="fedora-xfce">fedora-xfce</option>
            <option value="arch-xfce">arch-xfce</option>
            <option value="alpine-kde">alpine-kde</option>
            <option value="ubuntu-kde">ubuntu-kde</option>
            <option value="fedora-kde">fedora-kde</option>
            <option value="arch-kde">arch-kde</option>
            <option value="alpine-mate">alpine-mate</option>
            <option value="ubuntu-mate">ubuntu-mate</option>
            <option value="fedora-mate">fedora-mate</option>
            <option value="arch-mate">arch-mate</option>
            <option value="alpine-i3">alpine-i3</option>
            <option value="ubuntu-i3">ubuntu-i3</option>
            <option value="fedora-i3">fedora-i3</option>
            <option value="arch-i3">arch-i3</option>
            <option value="alpine-openbox">alpine-openbox</option>
            <option value="ubuntu-openbox">ubuntu-openbox</option>
            <option value="fedora-openbox">fedora-openbox</option>
            <option value="arch-openbox">arch-openbox</option>
            <option value="alpine-icewm">alpine-icewm</option>
            <option value="ubuntu-icewm">ubuntu-icewm</option>
            <option value="fedora-icewm">fedora-icewm</option>
            <option value="arch-icewm">arch-icewm</option>
        </select><br><br>
        <label for="suffix">Suffixe (ex: pmietlicki):</label>
        <input type="text" id="suffix" name="suffix" placeholder="pmietlicki" required><br><br>
        <label for="storage">Taille du PVC (ex: 5Gi):</label>
        <select name="storage" id="storage" required>
            <option value="1Gi">1Gi</option>
            <option value="2Gi" selected>2Gi</option>
            <option value="3Gi">3Gi</option>
            <option value="4Gi">4Gi</option>
            <option value="5Gi">5Gi</option>
            <option value="6Gi">6Gi</option>
            <option value="7Gi">7Gi</option>
            <option value="8Gi">8Gi</option>
            <option value="9Gi">9Gi</option>
            <option value="10Gi">10Gi</option>
        </select><br><br>
        <label for="memoryLimit">Limite de mémoire (ex: 2Gi):</label>
        <input type="text" id="memoryLimit" name="memoryLimit" value="2Gi" required><br><br>

        <label for="shmSize">Taille de SHM (ex: 2Gi):</label>
        <input type="text" id="shmSize" name="shmSize" value="2Gi" required><br><br>
        <label for="proxyAddress">Adresse du Proxy:</label>
        <input type="text" id="proxyAddress" name="proxyAddress" value="http://proxy.mondomaine.com"><br><br>

        <label for="proxyPort">Port du Proxy:</label>
        <input type="text" id="proxyPort" name="proxyPort" value="3128"><br><br>
        <label for="tools">Outils à installer:</label>
        <select multiple name="tools" id="tools">
            <option value="docker">docker</option>
            <option value="python">python</option>
            <option value="tmux">tmux</option>
            <option value="git">git</option>
            <option value="intellij">intellij</option>
            <option value="vscode">vscode</option>
        </select><br><br>
        <input type="button" value="Générer Configuration" onclick="generateYAML()">
    </form>

    <h3>Configuration YAML:</h3>
    <textarea id="output" rows="30" cols="100"></textarea>

    <script>
		function getInstallCommand(version, tool) {
			const packageManagers = {
				'apt-get': 'apt-get install -y',
				'dnf': 'dnf install -y',
				'pacman': 'pacman -Syu --noconfirm',
				'apk': 'apk add --no-cache'
			};

			let packageManager, installSnapd = '';
			if (version.includes('ubuntu') || version.includes('debian')) {
				packageManager = packageManagers['apt-get'];
				if (tool === 'intellij' || tool === 'vscode') {
					installSnapd = `${packageManager} snapd`;
				}
			} else if (version.includes('fedora')) {
				packageManager = packageManagers['dnf'];
				if (tool === 'intellij' || tool === 'vscode') {
					installSnapd = `${packageManager} snapd`;
				}
			} else if (version.includes('arch')) {
				packageManager = packageManagers['pacman'];
				if (tool === 'intellij' || tool === 'vscode') {
					// Installation spécifique de snapd pour Arch
					installSnapd = 'pacman -Syu --noconfirm snapd && systemctl enable --now snapd.socket';
				}
			} else if (version.includes('alpine')) {
				packageManager = packageManagers['apk'];
				// Alpine n'a pas de support officiel pour snapd, donc on ne met pas de commande pour installer snapd ici
			}

			switch (tool) {
				case 'docker':
					return `${installSnapd}\n${packageManager} docker`;
				case 'python':
					return `${installSnapd}\n${packageManager} python3 python3-pip`;
				case 'tmux':
					return `${installSnapd}\n${packageManager} tmux`;
				case 'git':
					return `${installSnapd}\n${packageManager} git`;
				case 'intellij':
				case 'vscode':
					// Assurez-vous que snapd est installé avant d'essayer d'utiliser snap pour installer ces outils
					return `${installSnapd}\nsnap install ${tool === 'intellij' ? 'intellij-idea-community --classic' : 'code --classic'}`;
				default:
					return `${installSnapd}\necho "${tool} n'est pas reconnu. Veuillez installer manuellement."`;
			}
		}
		
		function generateInstallScript(toolsArray, version) {
			if (toolsArray.length === 0) {
				return `echo "Aucun outil spécifique à déployer"`;
			}

			let installScript = '';
			toolsArray.forEach(tool => {
				installScript += getInstallCommand(version, tool) + "\n";
			});
			return installScript.trim();
		}

		
        function generateYAML() {
            const version = document.getElementById('version').value;
            const suffix = document.getElementById('suffix').value.trim();
            const storage = document.getElementById('storage').value;
            const toolsSelected = document.getElementById('tools').selectedOptions;
			      const toolsArray = Array.from(toolsSelected).map(option => option.value);
            const memoryLimit = document.getElementById('memoryLimit').value;
            const shmSize = document.getElementById('shmSize').value;
            const proxyURL = document.getElementById('proxyAddress').value;
            const proxyPort = document.getElementById('proxyPort').value;

            // Construction de la section des variables d'environnement de base
            let envVars = `
            - name: PUID
              value: '1000'
            - name: LANG
              value: 'fr_FR.UTF-8'
            - name: PGID
              value: '1000'
            - name: TZ
              value: 'Pacific/Noumea'
            - name: LANGUAGE
              value: 'fr_FR.UTF-8'
            - name: LC_ALL
              value: 'fr_FR.UTF-8'`;

             
            // Ajout des variables de proxy si elles sont fournies
            if (proxyURL && proxyPort) {
                envVars += `
            - name: HTTP_PROXY
              value: "${proxyURL}:${proxyPort}"
            - name: HTTPS_PROXY
              value: "${proxyURL}:${proxyPort}"
            - name: NO_PROXY
              value: "localhost,.svc.cluster.local"`;
            }
              
			if (!suffix) {
				alert("Veuillez entrer un suffixe.");
				return; // Stoppe l'exécution de la fonction si le suffixe est vide
			}
			
			let imageName = `linuxserver/rdesktop:${version}`;
			if (version === "alpine-xfce") {
				imageName = "linuxserver/rdesktop:latest";
			}
			
			// Étape 1: Générer le Script d'Installation
			const installScript = generateInstallScript(toolsArray, version);
			const formattedInstallScript = installScript
				.split('\n')
				.filter(line => line.trim() !== '') // Enlève les lignes vides
				.map(line => `    ${line}`) // Ajoute 4 espaces d'indentation
				.join('\n');

			// Étape 2: Intégrer le Script dans le ConfigMap
    const yamlConfigMap = `
apiVersion: v1
kind: ConfigMap
metadata:
  name: custom-script-${version}-${suffix}
  namespace: guacamole
data:
  install-script.sh: |+
    #!/bin/bash
${formattedInstallScript}`;


            const yaml = `---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  finalizers:
    - kubernetes.io/pvc-protection
  name: ${version}-${suffix}
  namespace: guacamole
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: ${storage}
  storageClassName: longhorn
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ${version}-${suffix}
  namespace: guacamole
spec:
  strategy:
    rollingUpdate:
      maxSurge: 25%
      maxUnavailable: 25%
    type: RollingUpdate
  selector:
    matchLabels:
      workload.user.cattle.io/workloadselector: apps.deployment-guacamole-${version}-${suffix}
  template:
    metadata:
      namespace: guacamole
      labels:
        workload.user.cattle.io/workloadselector: apps.deployment-guacamole-${version}-${suffix}
    spec:
      containers:
        - env:${envVars}
          image: ${imageName}
          imagePullPolicy: Always
          livenessProbe:
            failureThreshold: 5
            periodSeconds: 60
            successThreshold: 1
            tcpSocket:
              port: 3389
            timeoutSeconds: 1
          name: ${version}-${suffix}
          ports:
            - containerPort: 3389
              name: rdp
              protocol: TCP
          readinessProbe:
            failureThreshold: 5
            periodSeconds: 60
            successThreshold: 1
            tcpSocket:
              port: 3389
            timeoutSeconds: 1
          resources:
            limits:
              memory: ${memoryLimit}
            requests:
              memory: 1Gi
          securityContext:
            allowPrivilegeEscalation: true
            capabilities: {}
            privileged: false
            readOnlyRootFilesystem: false
            runAsNonRoot: false
          startupProbe:
            failureThreshold: 5
            periodSeconds: 120
            successThreshold: 1
            tcpSocket:
              port: 3389
            timeoutSeconds: 1
          terminationMessagePath: /dev/termination-log
          terminationMessagePolicy: File
          volumeMounts:
            - mountPath: /config
              name: vol-home
            - mountPath: /dev/shm
              name: vol-shm
            - mountPath: /custom-cont-init.d
              name: vol-init
              readOnly: true
      restartPolicy: Always
      volumes:
        - name: vol-home
          persistentVolumeClaim:
            claimName: ${version}-${suffix}
        - emptyDir:
            medium: Memory
            sizeLimit: ${shmSize}
          name: vol-shm
        - configMap:
            defaultMode: 420
            name: custom-script-${version}-${suffix}
            optional: false
          name: vol-init
---
apiVersion: v1
kind: Service
metadata:
  name: ${version}-${suffix}
  namespace: guacamole
spec:
  selector:
    workload.user.cattle.io/workloadselector: apps.deployment-guacamole-${version}-${suffix}
  ports:
    - name: rdp
      port: 3389
      protocol: TCP
      targetPort: 3389
  sessionAffinity: None
  type: ClusterIP
---` + yamlConfigMap;

            document.getElementById('output').value = yaml;
        }
    </script>
</body>
</html>

Bonus : Activation de vos images Windows via un serveur KMS émulé

Les images par défaut sont des versions d’évaluation, il est donc compliqué de les activer directement. Il va falloir les transformer.
Pour ce faire, vous pouvez suivre ce tutoriel : https://digital-licence.com/convertir-la-version-devaluation-windows-10-enterprise-ltsc-en-version-complete/

Les clefs de licence à utiliser pour faire la transformation vers la version de Windows souhaitée (d’une version 10 home à une enterprise par exemple) se trouve sur : https://learn.microsoft.com/fr-fr/windows-server/get-started/kms-client-activation-keys

Une fois effectué, vous pouvez très bien vous servir de votre VM alpine pour émuler un serveur KMS. Pour ce faire, il y a un dépôt Git : https://github.com/Wind4/vlmcsd

Il vous suffit d’installer les outils sous alpine, de compiler et d’exécuter le serveur émulé :

apk add --no-cache git gcc make cmake build-base linux-headers
git clone https://github.com/Wind4/vlmcsd.git
make
./bin/vlmcsd

Votre serveur kms va tourner sur le port 1688.

Vous retournez alors sur votre VM Windows et vous indiquez le serveur KMS qui tourne sur Linux dans un terminal :

slmgr /skms alpine-xfce-pmietlicki.guacamole.svc.cluster.local
slmgr /ato

Ainsi votre serveur KMS va valider votre licence Windows, ce n’est à utiliser qu’à des fins de test ou éducatives bien évidemment.

Il y a même une version dockerisée si vous voulez faire tourner l’image en continue dans un deployment kubernetes par exemple : https://github.com/Wind4/vlmcsd-docker