Créer une VM Linux dans Windows 10 Pro avec Hyper-V, le tout automatisé avec Vagrant

Objectif

On souhaite créer une VM Linux sous Windows 10 Pro. On va donc passer directement par l’outil Hyper-V (fourni avec Windows 10 Pro), et installer une VM Ubuntu par exemple. Mais cela impose pas mal de taches manuelles interactives, donc on va essayer de l’automatiser.

Pour cela on va utiliser un outil d’Infrastructure as Code, en l’occurrence Vagrant. D’autres existent comme Terraform, Ansible, Chef, Puppet…

Pourquoi Hyper-V ? En effet parfois on n’a pas la possibilité d’utiliser WSL 2 par exemple, ou ce dernier ne répond pas au besoin. Si on ne dispose pas de Windows 10 Pro et Hyper-V, on pourrait aussi simplement utiliser VirtualBox, compatible avec Vagrant.

Pour information, Vagrant est plutôt destiné à des environnement hors production, comme pour du développement ou des tests.

Installer Vagrant

Tout d’abord il faudra installer Vagrant pour votre OS, et en l’occurrence, pour Windows dans notre cas.

Activer Hyper-V

Bien sûr il faut s’assurer que Hyper-V est activé dans Windows. On pourra utiliser la commande suivante avec PowerShell :

Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V -All

Sinon on peut aussi l’activer via le menu Programmes et fonctionnalités, comme suit :

Fichier Vagrantfile

Le fichier Vagrantfile va permettre de décrire en lignes de code la machine que l’on veut créer, en essayant de garder une certaine portabilité entre les différentes plateformes.

On trouvera notamment le paramètre config.vm.box, indiquant à partir de quelle box ou image on veut créer notre VM. Un catalogue de box est disponible ici : https://app.vagrantup.com/boxes/search.

Dans notre cas, on va choisir generic/ubuntu2004, qui est compatible Hyper-V.

Exemple de script Vagrantfile :

# -*- mode: ruby -*-
# vi: set ft=ruby :
# All Vagrant configuration is done below. The "2" in Vagrant.configure
# configures the configuration version (we support older styles for
# backwards compatibility). Please don't change it unless you know what
# you're doing.
Vagrant.configure("2") do |config|
# The most common configuration options are documented and commented below.
# For a complete reference, please see the online documentation at
# https://docs.vagrantup.com.
# Every Vagrant development environment requires a box. You can search for
# boxes at https://vagrantcloud.com/search.
#config.vm.box = "base"
#config.vm.box = "generic/debian10"
#config.vm.box = "hashicorp/bionic64"
config.vm.box = "generic/ubuntu2004"
# Disable automatic box update checking. If you disable this, then
# boxes will only be checked for updates when the user runs
# `vagrant box outdated`. This is not recommended.
# config.vm.box_check_update = false
# Create a forwarded port mapping which allows access to a specific port
# within the machine from a port on the host machine. In the example below,
# accessing "localhost:8080" will access port 80 on the guest machine.
# NOTE: This will enable public access to the opened port
# config.vm.network "forwarded_port", guest: 80, host: 8080
# Create a forwarded port mapping which allows access to a specific port
# within the machine from a port on the host machine and only allow access
# via 127.0.0.1 to disable public access
# config.vm.network "forwarded_port", guest: 80, host: 8080, host_ip: "127.0.0.1"
# Create a private network, which allows host-only access to the machine
# using a specific IP.
# config.vm.network "private_network", ip: "192.168.33.10"
# Create a public network, which generally matched to bridged network.
# Bridged networks make the machine appear as another physical device on
# your network.
# config.vm.network "public_network"
# Share an additional folder to the guest VM. The first argument is
# the path on the host to the actual folder. The second argument is
# the path on the guest to mount the folder. And the optional third
# argument is a set of non-required options.
# config.vm.synced_folder "../data", "/vagrant_data"
config.vm.synced_folder "../data-test", "/vagrant_data"
# Provider-specific configuration so you can fine-tune various
# backing providers for Vagrant. These expose provider-specific options.
# Example for VirtualBox:
#
# View the documentation for the provider you are using for more
# information on available options.
# https://www.vagrantup.com/docs/providers/hyperv
config.vm.provider "hyperv" do |hv|
hv.cpus = "2"
hv.memory = "2048"
hv.enable_enhanced_session_mode = true
end
# https://www.vagrantup.com/docs/providers/virtualbox
config.vm.provider "virtualbox" do |vb|
vb.cpus = "2"
vb.memory = "2048"
end
# Enable provisioning with a shell script. Additional provisioners such as
# Ansible, Chef, Docker, Puppet and Salt are also available. Please see the
# documentation for more information about their specific syntax and use.
config.vm.provision "shell", inline: <<-SHELL
add-apt-repository ppa:git-core/ppa -y
apt-get update
apt-get install -y git git-svn svn2git
SHELL
end
view raw Vagrantfile hosted with ❤ by GitHub

Partage de fichiers

Cela pourra être pratique de mettre en place un partage de fichier entre l’hôte et la VM invitée, bien que ce ne soit pas obligatoire. C’est l’objet de la propriété :

config.vm.synced_folder "../data-test", "/vagrant_data"

Dans ce cas il faudra s’authentifier pour mettre en place le partage SMB.

Une fois dans la VM, on pourra voir ce dossier partagé dans la liste des mounts, et accéder au chemin /vagrant_data.

Exécution du script

On va maintenant demander à Vagrant de créer une VM correspondant au Vagrantfile. Pour ce faire, se placer dans le répertoire contenant le fichier Vagrantfile et lancer :

vagrant up --provider hyperv

Attention, si on ne spécifie pas le provider, par défaut Vagrant va essayer de créer une VM avec VirtualBox. Voir : https://www.vagrantup.com/docs/providers/default.

Lister les box Vagrant

Si la VM a bien été créée, une box a normalement été téléchargée. On peut vérifier çà avec cette commande :

vagrant box list

Vérification

On peut vérifier le statut de la VM (running si tout va bien) avec la commande suivante :

vagrant status

Il existe une commande globale pour lister toutes les VMs, que l’on peut exécuter de n’importe quel endroit :

vagrant global-status

Accéder à la VM

Il est maintenant possible d’accéder à la VM via SSH. Vous pouvez par exemple utiliser PuTTY. Vagrant propose aussi une commande pour cela :

vagrant ssh

A la création de la VM, Vagrant a créé une configuration SSH. On peut consulter ces infos (adresse IP, username, clé privée) avec la commande :

vagrant ssh-config

Arrêter la VM

On peut suspendre la VM avec la commande :

vagrant suspend

Détruire la VM

Si vous n’en avez plus besoin, vous pouvez aussi supprimer une VM :

vagrant destroy

Besoin de configurer un proxy ?

Allez voir cette réponse à la question How to use vagrant in a proxy environment?

C’est le plugin vagrant-proxyconf qui sera utilisé dans ce cas, et qui devra auparavant avoir été installé comme suit :

vagrant plugin install vagrant-proxyconf

Il faudra simplement penser à définir les variables d’environnement VAGRANT_HTTP_PROXY, VAGRANT_HTTPS_PROXY, voire VAGRANT_NO_PROXY. Comme précisé dans la réponse plus haut, les variables http_proxy et https_proxy sont nécessaires pour pouvoir télécharger la box sur internet.

La bonne nouvelle c’est que ce plugin permet non seulement de télécharger la box Vagrant nécessaire pour initialiser la VM, mais aussi de configurer le proxy de différents programmes.

Par exemple pour Apt c’est ce plugin qui va configurer le fichier /etc/apt/apt.conf.d/01proxy. Donc plus besoin de suivre cette manip : Configure proxy for APT.

Références externes

GitLab CI : utiliser son propre Runner dédié à Maven avec des settings personnalisés

Contexte

GitLab est un serveur d’hébergement de code source (SCM) dédié à Git. GitLab peut être utilisé avec une instance publique sur Internet (https://gitlab.com/), de façon similaire à GitHub (https://github.com/), ou bien avec une instance hébergée chez soi ou dans une entreprise (on premise).

GitLab intègre une partie « Intégration Continue », connue sous le nom de GitLab CI. Cela permet de vérifier que le logiciel peut être construit correctement, éventuellement d’exécuter des tests unitaires, voire de publier le logiciel. Et bien d’autres choses encore.

La particularité de GitLab CI est de déléguer l’exécution du pipeline d’intégration continue, découpé en tâches (nommées jobs), à d’autres machines, appelées Runners. Ces runners peuvent être mis à disposition d’une instance GitLab via des shared runners, mais on peut également dédier des runners spécifiques à son projet ou son groupe de projets.

On peut ainsi exécuter des runners sur sa propre machine, en installant des GitLab Runners, que l’on soit sous Windows, Mac, Linux, ou même avec une infrastructure plus complexe comme Kubernetes.

Cas d’utilisation

L’objectif ici est de permettre de construire un projet Java dans GitLab CI, à l’aide de Maven.

Bien souvent en entreprise on va aussi utiliser notre propre dépôt (repository) de librairies Java compatible avec Maven notamment. Cela permet à la fois de servir de cache et de proxy pour tous les développeurs qui auraient besoin d’accéder à des librairies externes, disponibles sur Internet. Cela va également permettre d’héberger nos propres librairies ou artefacts, sur un serveur privé. Pour ce faire, on peut par exemple utiliser Sonatype Nexus. Il existe aussi d’autres solutions, comme Artifactory.

Pour faciliter les choses, on souhaite également utiliser Docker, une solution à base de containers.

L’enjeu sera essentiellement ici de pouvoir exécuter les tâches Maven dans GitLab CI, en configurant le settings.xml de Maven afin d’utiliser un repository Nexus interne.

L’exécution de Maven se fera dans chaque job à l’aide d’un executor défini dans le GitLab Runner et configuré pour utiliser Docker. Ainsi, chaque exécution de job consistera à instancier un container Docker et exécuter un script à l’intérieur. On tentera donc au maximum d’utiliser les images Maven officielles présentes sur Docker Hub pour cela. Un des avantages de Docker est notamment d’éviter l’installation de toute une série d’outils sur la machine hôte, comme un JDK et une version de Maven dans ce cas. Ces derniers sont simplement packagés dans l’image Docker correspondante, en isolation de la machine hôte.

Environnement

Dans cet article on va considérer que l’on travaille sur une machine Linux. C’est possible de le faire fonctionner sous Windows, mais cela rajoute quelques contraintes, et le cas de Linux est sûrement plus pertinent en production.

Installation du runner

Si ce n’est déjà fait, il faut installer le GitLab Runner qui fonctionnera comme un service.
Globalement on va suivre la procédure expliquée à cette page : https://docs.gitlab.com/runner/install/linux-manually.html. Pour Ubuntu / Debian on aura donc un package nommé gitlab-runner_x86_64.deb, à installer avec la commande dpkg.
Il est aussi possible de télécharger le binaire pour d’autres architectures comme expliqué dans le lien plus haut.

Pour vérifier que le service est actif, exécuter au choix :

  • sudo gitlab-runner status
  • service gitlab-runner status

Si le service n’est pas démarré, exécuter :

  • sudo gitlab-runner start

Configuration du runner

Pour configurer le runner, on va en général utiliser le fichier config.toml. Voir : https://docs.gitlab.com/runner/configuration/.
Par exemple le paramètre concurrent va définir le nombre de jobs exécutés simultanément.

Enregistrement des runners

Ensuite, un ou plusieurs runners peuvent être enregistrés. Dans notre cas, on utilisera les runners spécifiques à un projet. Il faudra au préalable aller dans le projet GitLab dans la section Settings > CI/CD et ouvrir Runners pour récupérer l’URL et le token.
Par exemple, on va enregistrer un runner spécifique aux jobs Maven.

sudo gitlab-runner register \
  --non-interactive \
  --url "https://gitlab.com/" \
  --registration-token "PROJECT_REGISTRATION_TOKEN" \
  --executor "docker" \
  --docker-image maven:latest \
  --description "Runner Maven" \
  --tag-list "maven" \
  --run-untagged="false" \
  --locked="false" \
  --access-level="not_protected"

La valeur de registration-token doit être valorisée avec la valeur du token du projet.

On veut aussi restreindre les images Docker utilisables dans ce runner. On va donc rajouter

--allowed_images="maven:*"

pour n’utiliser que des images Docker Maven officielles.

Lister les runners enregistrés

sudo gitlab-runner list

La liste des runners configurés doit s’afficher.

Tester l’exécution du pipeline dans le runner

Normalement à ce stade le runner est enregistré et prêt à exécuter des jobs du projet. Si un fichier .gitlab-ci.yml a déjà été ajouté aux sources du projet, vous pouvez tester la bonne exécution du pipeline en rajoutant un commit dans le projet ou en forçant son exécution.

Comme on a associé un tag à notre runner, il ne faut pas oublier de le spécifier dans la déclaration du job, comme par exemple :

build:
  stage: build
  image: maven:3.6-jdk-8
  script:
    - mvn $MAVEN_CLI_OPTS verify
  tags:
    - maven

Problème de proxy ?

Bon si vous êtes derrière un proxy, il est probable de rencontrer des problèmes. Il faudra peut-être regarder ici : Running GitLab Runner behind a proxy.

Utilisation d’un registry Docker privé ?

Il est aussi possible que vous deviez passer par un registry Docker privé pour le téléchargement des images Docker. Dans ce cas GitLab Runner devra être configuré en conséquence, voir Using a private container registry. On pourra également restreindre l’origine des images autorisées (allowed_images) à un registry privé.

Voir les détails spécifiques à Docker dans Define an image from a private Container Registry et en particulier la définition de la variable DOCKER_AUTH_CONFIG (credentials + registry).

Concernant la configuration spécifique à Docker pour l’utilisation d’un registry privé en mode proxy, voir Registry as a pull through cache.

Voir aussi ces issues :

Etape suivante : utiliser un repository Maven privé

Ensuite nous souhaitons que notre job, exécuté dans un container Docker avec une image Maven utilise un repository Maven spécifique.

Avant tout, on va démarrer un serveur Sonatype Nexus pour se mettre en conditions réelles.

Avec Docker c’est assez simple :

docker run -d -p 8081:8081 --name nexus sonatype/nexus3

Une fois démarré, un serveur Nexus doit être accessible à http://localhost:8081.

Configuration des settings Maven

On va ensuite définir des settings personnalisés pour Maven avec un fichier custom-settings.xml comme suit :

<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 https://maven.apache.org/xsd/settings-1.0.0.xsd">
	<localRepository/>
	<mirrors>
		<mirror>
			<id>private-nexus-repo</id>
			<name>My Nexus Maven Repository</name>
			<url>http://localhost:8081/repository/maven-public/</url>
			<mirrorOf>central</mirrorOf>
		</mirror>
	</mirrors>
</settings>

Ces settings doivent ensuite être passés au conteneur Docker au moyen d’un bind mount en utilisant les volumes Docker. Les volumes sont configurés au niveau du runner GitLab.

Dans la configuration config.toml, on ajoutera ou modifiera donc cette section :

  [runners.docker]
    ...
    volumes = ["/cache", "/host/path/custom-settings.xml:/root/.m2/settings.xml:ro"]

En pratique, on peut valider que les settings Maven sont corrects en exécutant simplement un container Docker comme ici :

docker run --rm -v $(pwd)/custom-settings.xml:/root/.m2/settings.xml:ro maven:3 mvn help:effective-settings

Au minimum le plugin help devrait être téléchargé via Nexus, ainsi que ses dépendances. Cela permet aussi de valider que le montage de volume du fichier settings.xml est correct et bien pris en compte dans le container. Un chemin absolu est nécessaire quand on passe par Docker CLI (contrairement à Docker Compose).
Le résultat de la commande précédente devrait ressembler à çà au final :

[INFO] 
Effective user-specific configuration settings:
<?xml version="1.0" encoding="UTF-8"?>
<!-- ====================================================================== -->
<!--                                                                        -->
<!-- Generated by Maven Help Plugin on 2021-03-02T18:12:40Z                 -->
<!-- See: http://maven.apache.org/plugins/maven-help-plugin/                -->
<!--                                                                        -->
<!-- ====================================================================== -->
<!-- ====================================================================== -->
<!--                                                                        -->
<!-- Effective Settings for 'root' on '19791eb218d0'                        -->
<!--                                                                        -->
<!-- ====================================================================== -->
<settings xmlns="http://maven.apache.org/SETTINGS/1.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.1.0 http://maven.apache.org/xsd/settings-1.1.0.xsd">
  <localRepository>/root/.m2/repository</localRepository>
  <mirrors>
    <mirror>
      <mirrorOf>central</mirrorOf>
      <name>My Nexus Maven Repository</name>
      <url>http://172.17.0.1:8081/repository/maven-public/</url>
      <id>private-nexus-repo</id>
    </mirror>
  </mirrors>
  <pluginGroups>
    <pluginGroup>org.apache.maven.plugins</pluginGroup>
    <pluginGroup>org.codehaus.mojo</pluginGroup>
  </pluginGroups>
</settings>
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  59.360 s
[INFO] Finished at: 2021-03-02T18:12:40Z
[INFO] ------------------------------------------------------------------------

Accès au serveur Nexus à partir du conteneur Docker

Attention si le conteneur souhaite accéder à un service hébergé sur son hôte, il ne pourra pas résoudre localhost, et il faudra remplacer par exemple localhost par son ip résolue. Voir :

Tester avec cette commande par exemple :

ip addr show docker0

Résultat attendu

Normalement à ce point, tout devrait fonctionner. Dans le log du job Maven du pipeline on devrait avoir quelque chose comme çà :

Running with gitlab-runner 13.9.0 (2ebc4dc4)
  on Runner Maven XHUSESdz
Preparing the "docker" executor
00:06
Using Docker executor with image maven:3.6-jdk-8 ...
Pulling docker image maven:3.6-jdk-8 ...
Using docker image sha256:75cbe9c11be24bae1cb8064cbc33c26df79321b5047c30ff8e29abcd2e5289a9 for maven:3.6-jdk-8 with digest maven@sha256:d4a4bb5d83a95c012615c5294f665ee042e721298c941eea32db3ea0ca588e9b ...
Preparing environment
00:02
Running on runner-xhusesdz-project-23778885-concurrent-0 via guillaume-F3JP...
Getting source from Git repository
00:05
Fetching changes with git depth set to 50...
Reinitialized existing Git repository in /builds/ghusta/maven-git-flow-test/.git/
Checking out c170a61a as feature/gitlab-ci-tags-custom-runner...
Removing .m2/
Removing target/
Skipping Git submodules setup
Restoring cache
00:03
Checking cache for default...
No URL provided, cache will not be downloaded from shared cache server. Instead a local version of cache will be extracted. 
Successfully extracted cache
Executing "step_script" stage of the job script
00:10
Using docker image sha256:75cbe9c11be24bae1cb8064cbc33c26df79321b5047c30ff8e29abcd2e5289a9 for maven:3.6-jdk-8 with digest maven@sha256:d4a4bb5d83a95c012615c5294f665ee042e721298c941eea32db3ea0ca588e9b ...
$ mvn $MAVEN_CLI_OPTS verify
Apache Maven 3.6.3 (cecedd343002696d0abb50b32b541b8a6ba2883f)
Maven home: /usr/share/maven
Java version: 1.8.0_282, vendor: Oracle Corporation, runtime: /usr/local/openjdk-8/jre
Default locale: en, platform encoding: UTF-8
OS name: "linux", version: "5.4.0-66-generic", arch: "amd64", family: "unix"
3361 [INFO] Error stacktraces are turned on.
3477 [INFO] Scanning for projects...
...
7330 [INFO] ------------------------------------------------------------------------
7336 [INFO] BUILD SUCCESS
7337 [INFO] ------------------------------------------------------------------------
7340 [INFO] Total time:  3.910 s
7345 [INFO] Finished at: 2021-02-28T21:08:20Z
7348 [INFO] ------------------------------------------------------------------------
Saving cache for successful job
00:01
Creating cache default...
.m2/repository: found 977 matching files and directories 
Archive is up to date!                             
Created cache
Cleaning up file based variables
00:01
Job succeeded

Et voilà çà marche !

Et n’importe quelle image Docker Maven peut être configurée dans le fichier .gitlab-ci.yml, au choix du développeur. Comme par exemple :

  • maven
  • maven:3.6
  • maven:3-jdk-8
  • maven:3-openjdk-11
  • maven:3-openjdk-17-slim
  • etc…