first sync

This commit is contained in:
hcornet committed 2026-09-12 10:09:00 +02:00
1 parent 6087245e5c
commit b046f4054e
15 files changed
+721 -103

No files matched your search

-39
View File
@@ -1,39 +0,0 @@
name: terraform validation
on:
push:
branches: [main]
pull_request:
types: [opened, reopened, edited, synchronize]
jobs:
validate:
name: 'Terraform'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.7.5
- name: Terraform fmt
id: fmt
run: terraform fmt -check -diff -recursive
continue-on-error: true
- name: Terraform Init
id: init
run: terraform init
# working-directory: examples
- name: Terraform Validate
id: validate
run: terraform validate
# working-directory: examples
- name: Terraform Plan
id: plan
run: terraform plan
+6 -38
View File
@@ -1,41 +1,9 @@
# ---> VisualStudioCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
# Local History for Visual Studio Code
.history/
# Built Visual Studio Code Extensions
*.vsix
#
*.exe
# Local .terraform directories
**/.terraform/*
# .tfstate files
.terraform/
*.tfstate
*.tfstate.*
# Crash log files
crash.log
crash.*.log
# Exclude all .tfvars files, which are likely to contain sensitive data
*.tfvars
*.tfvars.json
# Ignore transient lock info files created by terraform apply
.terraform.tfstate.lock.info
# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan
*tfplan*
# Ignore CLI configuration files
.terraformrc
terraform.rc
terraform.tfvars
*.auto.tfvars
generated/
override.tf
override.tf.json
+104 -26
View File
@@ -1,39 +1,117 @@
# centreon-terraform — VM Debian 12 minimale pour Centreon
# ${REPO_NAME}
Crée sur le nœud Proxmox `pve` une VM Debian 12 réduite au strict nécessaire, durcie
par cloud-init, et prête à être reprise par Ansible (dépôt `centreon-ansible`).
${REPO_DESCRIPTION}
Ce dépôt ne connaît rien de Centreon : il produit un serveur, rien d'autre.
# A propos de
## Ce que le code met en place
# Prérequis
| Point | Choix retenu |
|---|---|
| Système | Debian 12 *genericcloud* (image officielle, ~350 Mo, sans noyau cloud-specific inutile) |
| Pourquoi pas Debian 13 | Centreon 25.10 ne supporte que Debian 12 |
| Comptes | un seul compte unix (`ansible`), sans mot de passe, sudo NOPASSWD, clé publique uniquement |
| root | connexion SSH refusée, mot de passe verrouillé |
| SSH | mot de passe et clavier-interactif désactivés, `AuthenticationMethods publickey`, `AllowUsers` limité au compte d'administration |
| Services exposés | **22/tcp et ICMP uniquement**, et seulement depuis `ssh_allowed_cidrs` |
| Pare-feu | nftables, politique `drop` en entrée et en transit, avec un répertoire `/etc/nftables.d/` qu'Ansible viendra compléter |
| Mises à jour | `unattended-upgrades` activé |
| Sysctl | redirections ICMP, source routing, dmesg et kptr restreints |
| Agent | `qemu-guest-agent` (remontée des IP et arrêt propre côté Proxmox) |
Vous avez besoin d'une installation docker fonctionnelle.
Rien d'autre n'écoute : pas de serveur mail local, pas de rpcbind, pas d'exim —
l'image *genericcloud* n'en embarque aucun.
https://developer.hashicorp.com/terraform/install?product_intent=terraform
## Prérequis
# Démarrage manuel
1. **Jeton d'API Proxmox.** Le jeton `terraform@pve!provisioner` existant convient, à
condition que son rôle porte au minimum :
`VM.Allocate`, `VM.Config.*`, `VM.PowerMgmt`, `VM.Audit`, `VM.Console`,
`VM.GuestAgent.Audit`, `Datastore.Allocate`, `Datastore.AllocateSpace`,
`Datastore.AllocateTemplate`, `Datastore.Audit`, `Sys.Audit`, `SDN.Use`.
`SDN.Use` est nécessaire depuis PVE 8.2 pour attacher une carte à un bridge.
~~~bash
git clone https://gitea.tips-of-mine.com${REPO_LINK}.git
cd ${TEMPLATE_NAME}
~~~
2. **Accès SSH au nœud.** Le dépôt du snippet cloud-init passe par SSH : l'API
Proxmox ne sait pas téléverser dans le contenu `snippets`. Le compte utilisé doit
pouvoir écrire dans `/var/lib/vz/snippets`.
# Utilisation du repository
3. **Contenu `snippets` activé** sur le stockage `local` :
`pvesm set local --content images,rootdir,vztmpl,backup,iso,snippets`
(à vérifier une fois : `pvesm status --storage local`).
init
~~~bash
terraform init
~~~
4. **Réservation d'adresse.** `10.0.5.20` doit être exclue de la plage DHCP
d'OPNsense, sinon le bail pourra être attribué à une autre machine.
validation
~~~bash
terraform validate
~~~
## Secrets
plan
~~~bash
terraform plan
~~~
Aucun secret n'est écrit dans les fichiers `.tf` ni dans `terraform.tfvars` :
# Buy me a coffe
<a href='https://ko-fi.com/R5R2KNI3N' target='_blank'><img height='36' style='border:0px;height:36px;' src='https://storage.ko-fi.com/cdn/kofi4.png?v=3' border='0' alt='Buy Me a Coffee at ko-fi.com' /></a>
```bash
export TF_VAR_pve_api_token='terraform@pve!provisioner=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
export TF_VAR_pve_ssh_password='...'
```
Si le projet rejoint la CI Gitea, ces deux valeurs se lisent dans OpenBao comme pour
`proxmox-create-vm`, en réutilisant `secret/proxmox`.
## Exécution sans installer Terraform
```bash
cp terraform.tfvars.example terraform.tfvars # puis adapter, surtout ssh_public_keys
docker run --rm -it \
-v "$PWD":/work -w /work \
-e TF_VAR_pve_api_token -e TF_VAR_pve_ssh_password \
hashicorp/terraform:latest init
docker run --rm -it \
-v "$PWD":/work -w /work \
-e TF_VAR_pve_api_token -e TF_VAR_pve_ssh_password \
hashicorp/terraform:latest apply
```
Sous PowerShell, remplacer `"$PWD"` par `${PWD}` et passer les variables avec
`-e TF_VAR_pve_api_token=$env:TF_VAR_pve_api_token`.
L'état reste local (`terraform.tfstate`) : c'est une plateforme de test, il n'y a pas
de backend distant. Le passage sur le backend S3 se fait en ajoutant un `state.tf`,
comme dans `proxmox-create-vm`.
## Après l'apply
`terraform apply` produit :
- la VM 120 démarrée, qui exécute cloud-init pendant **3 à 6 minutes** (mise à jour
complète du système comprise) ;
- `generated/inventory.yml`, l'inventaire Ansible correspondant.
Vérification :
```bash
ssh [email protected] 'cloud-init status --wait && sudo nft list chain inet filter input'
```
La sortie attendue ne contient que `lo`, `established,related`, l'ICMP et le port 22.
## Points d'attention
- **cloud-init ne rejoue rien.** Modifier `templates/user-data.yaml.tftpl` après
création ne change pas la VM existante : le snippet garde le même nom, donc le même
identifiant, et Terraform ne voit même pas de différence. Pour repartir d'un
système propre : `terraform destroy` puis `apply`.
- **Image Debian.** `overwrite = false` évite de retélécharger 350 Mo à chaque apply.
Debian republie régulièrement l'image `latest` sous le même nom : si une version
plus récente est souhaitée, supprimer le fichier sur le nœud et relancer l'apply.
Le paramètre `debian_image_checksum` est disponible mais laissé vide, une empreinte
figée cassant à chaque respin de l'image.
- **Extension `.img`.** Proxmox n'accepte que `.iso` et `.img` dans le contenu `iso` :
le `.qcow2` téléchargé est donc renommé, son format réel étant inchangé.
- **Redémarrage.** Si cloud-init installe un nouveau noyau, la VM continue de tourner
sur l'ancien. Un `sudo reboot` avant de lancer Ansible est une bonne habitude.
## Suite
```bash
cp generated/inventory.yml ../centreon-ansible/inventory/hosts.yml
```
+26
View File
@@ -0,0 +1,26 @@
locals {
fqdn = "${var.vm_hostname}.${var.dns_domain}"
}
# Le fichier est déposé dans le contenu "snippets" du stockage, via SSH.
# Attention : cloud-init ne rejoue la configuration qu'au premier démarrage.
# Modifier ce snippet après coup n'a donc aucun effet sur une VM déjà installée.
resource "proxmox_virtual_environment_file" "user_data" {
content_type = "snippets"
datastore_id = var.snippet_datastore_id
node_name = var.pve_node
source_raw {
file_name = "${var.vm_hostname}-user-data.yaml"
data = templatefile("${path.module}/templates/user-data.yaml.tftpl", {
hostname = var.vm_hostname
fqdn = local.fqdn
timezone = var.timezone
locale = var.locale
ansible_user = var.ansible_user
ssh_keys = var.ssh_public_keys
admin_cidrs = var.ssh_allowed_cidrs
})
}
}
View File
Whitespace-only changes.
View File
Whitespace-only changes.
+121
View File
@@ -0,0 +1,121 @@
resource "proxmox_virtual_environment_download_file" "debian_cloud_image" {
content_type = "iso"
datastore_id = var.image_datastore_id
node_name = var.pve_node
url = var.debian_image_url
file_name = var.debian_image_file_name
checksum = var.debian_image_checksum
checksum_algorithm = var.debian_image_checksum == null ? null : var.debian_image_checksum_algorithm
# L'image n'est pas retéléchargée à chaque apply, mais un fichier déjà
# présent et non géré par Terraform est repris plutôt que de faire échouer le plan.
overwrite = false
overwrite_unmanaged = true
upload_timeout = 1800
}
resource "proxmox_virtual_environment_vm" "centreon" {
name = var.vm_hostname
description = "Serveur central Centreon - géré par Terraform, ne pas modifier à la main"
tags = ["terraform", "debian12", "centreon"]
node_name = var.pve_node
vm_id = var.vm_id
on_boot = true
started = true
stop_on_destroy = true
machine = "q35"
bios = "seabios"
scsi_hardware = "virtio-scsi-single"
boot_order = ["scsi0"]
operating_system {
type = "l26"
}
agent {
enabled = true
trim = true
}
cpu {
cores = var.vm_cores
type = "host"
}
memory {
dedicated = var.vm_memory
}
disk {
datastore_id = var.vm_datastore_id
import_from = proxmox_virtual_environment_download_file.debian_cloud_image.id
interface = "scsi0"
size = var.vm_disk_size
file_format = "qcow2"
discard = "on"
iothread = true
ssd = true
}
network_device {
bridge = var.vm_bridge
model = "virtio"
}
# Console série : permet un « qm terminal 120 » depuis le nœud si le réseau
# de la VM est cassé, sans ouvrir de service supplémentaire.
serial_device {}
initialization {
datastore_id = var.vm_datastore_id
interface = "ide2"
dns {
domain = var.dns_domain
servers = var.dns_servers
}
ip_config {
ipv4 {
address = "${var.vm_ipv4_address}/${var.vm_ipv4_prefix}"
gateway = var.vm_ipv4_gateway
}
}
user_data_file_id = proxmox_virtual_environment_file.user_data.id
}
lifecycle {
# Proxmox renvoie un type de machine versionné (pc-q35-11.0+pve2) et
# réordonne le boot : sans cela, chaque plan proposerait une modification.
ignore_changes = [
machine,
boot_order,
]
}
}
resource "local_file" "ansible_inventory" {
filename = var.ansible_inventory_path
file_permission = "0644"
content = yamlencode({
all = {
children = {
centreon_central = {
hosts = {
(var.vm_hostname) = {
ansible_host = var.vm_ipv4_address
ansible_user = var.ansible_user
}
}
}
}
}
})
depends_on = [proxmox_virtual_environment_vm.centreon]
}
View File
Whitespace-only changes.
+24
View File
@@ -0,0 +1,24 @@
output "vm_id" {
description = "VMID de la machine créée."
value = proxmox_virtual_environment_vm.centreon.vm_id
}
output "vm_ipv4_address" {
description = "Adresse IPv4 fixe de la machine."
value = var.vm_ipv4_address
}
output "vm_fqdn" {
description = "Nom pleinement qualifié de la machine."
value = local.fqdn
}
output "ssh_command" {
description = "Commande de connexion une fois cloud-init terminé."
value = "ssh ${var.ansible_user}@${var.vm_ipv4_address}"
}
output "ansible_inventory_file" {
description = "Inventaire généré, à recopier dans le dépôt Ansible."
value = local_file.ansible_inventory.filename
}
View File
Whitespace-only changes.
+18
View File
@@ -0,0 +1,18 @@
provider "proxmox" {
endpoint = var.pve_endpoint
api_token = var.pve_api_token
insecure = var.pve_insecure
# Le dépôt d'un snippet cloud-init passe obligatoirement par SSH :
# l'API Proxmox ne permet pas de téléverser dans le contenu "snippets".
ssh {
agent = false
username = var.pve_ssh_username
password = var.pve_ssh_password
node {
name = var.pve_node
address = var.pve_node_address
}
}
}
+155
View File
@@ -0,0 +1,155 @@
#cloud-config
# Généré par Terraform — toute modification manuelle sera écrasée.
# Rappel : ce fichier n'est appliqué qu'au tout premier démarrage de la VM.
hostname: ${hostname}
fqdn: ${fqdn}
prefer_fqdn_over_hostname: true
manage_etc_hosts: true
timezone: ${timezone}
# La clé "users" remplace la liste par défaut : le compte "debian" livré avec
# l'image cloud n'est donc jamais créé. Un seul compte existe sur la machine,
# sans mot de passe (lock_passwd), accessible uniquement par clé.
users:
- name: ${ansible_user}
gecos: Compte d administration (Ansible)
groups: [sudo]
shell: /bin/bash
lock_passwd: true
sudo: "ALL=(ALL) NOPASSWD:ALL"
ssh_authorized_keys:
%{ for key in ssh_keys ~}
- "${key}"
%{ endfor ~}
disable_root: true
ssh_pwauth: false
ssh_deletekeys: true
ssh_genkeytypes: [ed25519, rsa]
package_update: true
package_upgrade: true
# Strict minimum : agent invité, pare-feu, mises à jour de sécurité,
# et les prérequis d'Ansible et de l'installateur Centreon.
packages:
- qemu-guest-agent
- nftables
- unattended-upgrades
- apt-listchanges
- locales
- python3-apt
- ca-certificates
- curl
- wget
- gnupg2
- lsb-release
- apt-transport-https
write_files:
- path: /etc/ssh/sshd_config.d/99-durcissement.conf
owner: root:root
permissions: "0600"
content: |
# Durcissement SSH — cloud-init
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitEmptyPasswords no
PubkeyAuthentication yes
AuthenticationMethods publickey
AllowUsers ${ansible_user}
MaxAuthTries 3
LoginGraceTime 30
X11Forwarding no
AllowAgentForwarding no
AllowTcpForwarding no
ClientAliveInterval 300
ClientAliveCountMax 2
- path: /etc/nftables.conf
owner: root:root
permissions: "0640"
content: |
#!/usr/sbin/nft -f
# Jeu de règles principal — cloud-init.
# En entrée, seuls SSH et ICMP sont ouverts. Les ports applicatifs
# (web, SNMP, gorgone) sont ajoutés par Ansible dans /etc/nftables.d/.
flush ruleset
table inet filter {
set reseaux_admin {
type ipv4_addr
flags interval
elements = { ${join(", ", admin_cidrs)} }
}
chain input {
type filter hook input priority filter; policy drop;
iif "lo" accept
ct state established,related accept
ct state invalid drop
ip protocol icmp icmp type { echo-request, destination-unreachable, time-exceeded, parameter-problem } accept
ip6 nexthdr ipv6-icmp accept
tcp dport 22 ip saddr @reseaux_admin ct state new accept
include "/etc/nftables.d/*.nft"
counter comment "paquets rejetes en entree"
}
chain forward {
type filter hook forward priority filter; policy drop;
}
chain output {
type filter hook output priority filter; policy accept;
}
}
- path: /etc/nftables.d/00-placeholder.nft
owner: root:root
permissions: "0640"
content: |
# Fragments de règles ajoutés par Ansible. Ne rien mettre ici.
- path: /etc/apt/apt.conf.d/20auto-upgrades
owner: root:root
permissions: "0644"
content: |
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
- path: /etc/sysctl.d/99-durcissement.conf
owner: root:root
permissions: "0644"
content: |
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.all.accept_source_route = 0
net.ipv6.conf.all.accept_redirects = 0
net.ipv4.icmp_echo_ignore_broadcasts = 1
net.ipv4.tcp_syncookies = 1
kernel.dmesg_restrict = 1
kernel.kptr_restrict = 2
fs.protected_hardlinks = 1
fs.protected_symlinks = 1
runcmd:
- sed -i "s|^# *${locale} UTF-8|${locale} UTF-8|" /etc/locale.gen
- locale-gen
- update-locale LANG=${locale}
- sysctl --system > /dev/null
- systemctl enable --now qemu-guest-agent
- systemctl enable --now nftables
- systemctl enable --now unattended-upgrades
- systemctl restart ssh
- apt-get -y autoremove --purge
- apt-get -y clean
final_message: "cloud-init termine apres $UPTIME s - ${fqdn} est pret pour Ansible"
+30
View File
@@ -0,0 +1,30 @@
# Copier en terraform.tfvars et adapter.
# Les deux secrets ne sont PAS dans ce fichier : ils passent par l'environnement
# export TF_VAR_pve_api_token='terraform@pve!provisioner=xxxxxxxx-...'
# export TF_VAR_pve_ssh_password='...'
pve_endpoint = "https://10.0.4.5:8006/"
pve_node = "pve"
pve_node_address = "10.0.4.5"
pve_insecure = true
vm_id = 120
vm_hostname = "centreon"
vm_bridge = "vmbr3"
vm_ipv4_address = "10.0.5.20"
vm_ipv4_gateway = "10.0.5.1"
dns_servers = ["10.0.5.1"]
dns_domain = "tips-of-mine.local"
vm_cores = 2
vm_memory = 4096
vm_disk_size = 40
ansible_user = "ansible"
ssh_public_keys = [
"ssh-ed25519 AAAA... hcornet@poste",
]
ssh_allowed_cidrs = ["10.0.4.0/24", "10.0.5.0/24"]
+223
View File
@@ -0,0 +1,223 @@
############################################
# Accès au nœud Proxmox
############################################
variable "pve_endpoint" {
description = "URL de l'API Proxmox (ex. https://10.0.4.5:8006/)."
type = string
}
variable "pve_api_token" {
description = "Jeton d'API au format user@realm!tokenid=uuid. À fournir par TF_VAR_pve_api_token."
type = string
sensitive = true
}
variable "pve_insecure" {
description = "Ne pas vérifier le certificat TLS de l'API (nécessaire si l'endpoint est appelé par IP)."
type = bool
default = true
}
variable "pve_node" {
description = "Nom du nœud Proxmox cible."
type = string
default = "pve"
}
variable "pve_node_address" {
description = "Adresse IP du nœud, utilisée pour la connexion SSH de dépôt du snippet."
type = string
default = "10.0.4.5"
}
variable "pve_ssh_username" {
description = "Compte SSH sur le nœud Proxmox (doit pouvoir écrire dans le répertoire des snippets)."
type = string
default = "root"
}
variable "pve_ssh_password" {
description = "Mot de passe SSH du nœud. À fournir par TF_VAR_pve_ssh_password."
type = string
sensitive = true
}
############################################
# Image Debian
############################################
variable "debian_image_url" {
description = "URL de l'image cloud Debian 12 (Centreon 25.10 ne supporte que Debian 12)."
type = string
default = "https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-genericcloud-amd64.qcow2"
}
variable "debian_image_file_name" {
description = "Nom du fichier stocké sur le nœud. L'extension .img est imposée par Proxmox pour le contenu iso."
type = string
default = "debian-12-genericcloud-amd64.img"
}
variable "debian_image_checksum" {
description = "Empreinte de l'image (facultatif). Laisser à null pour ne pas vérifier."
type = string
default = null
sensitive = false
}
variable "debian_image_checksum_algorithm" {
description = "Algorithme de l'empreinte fournie (sha256, sha512...)."
type = string
default = "sha512"
}
variable "image_datastore_id" {
description = "Stockage recevant l'image téléchargée (doit accepter le contenu iso)."
type = string
default = "local"
}
variable "snippet_datastore_id" {
description = "Stockage recevant le snippet cloud-init (doit accepter le contenu snippets)."
type = string
default = "local"
}
############################################
# Définition de la VM
############################################
variable "vm_id" {
description = "VMID de la machine."
type = number
default = 120
}
variable "vm_hostname" {
description = "Nom court de la machine (nom de VM et hostname système)."
type = string
default = "centreon"
}
variable "vm_datastore_id" {
description = "Stockage du disque système et du disque cloud-init."
type = string
default = "local"
}
variable "vm_cores" {
description = "Nombre de cœurs. 2 est le plancher réaliste pour un central avec base locale."
type = number
default = 2
validation {
condition = var.vm_cores >= 2
error_message = "Centreon a besoin d'au moins 2 cœurs (moteur + base + PHP-FPM)."
}
}
variable "vm_memory" {
description = "Mémoire en Mio."
type = number
default = 4096
validation {
condition = var.vm_memory >= 4096
error_message = "Centreon a besoin d'au moins 4096 Mio : en dessous, l'installation web tombe en timeout."
}
}
variable "vm_disk_size" {
description = "Taille du disque système en Gio."
type = number
default = 40
}
variable "vm_bridge" {
description = "Bridge de rattachement de la carte réseau."
type = string
default = "vmbr3"
}
############################################
# Réseau
############################################
variable "vm_ipv4_address" {
description = "Adresse IPv4 fixe de la VM."
type = string
default = "10.0.5.20"
}
variable "vm_ipv4_prefix" {
description = "Longueur du préfixe IPv4."
type = number
default = 24
}
variable "vm_ipv4_gateway" {
description = "Passerelle IPv4 (interface LAN d'OPNsense)."
type = string
default = "10.0.5.1"
}
variable "dns_servers" {
description = "Serveurs DNS poussés par cloud-init."
type = list(string)
default = ["10.0.5.1"]
}
variable "dns_domain" {
description = "Domaine de recherche et suffixe du FQDN."
type = string
default = "tips-of-mine.local"
}
############################################
# Système et accès
############################################
variable "timezone" {
description = "Fuseau horaire du système. Centreon reprend ce réglage pour PHP à l'installation."
type = string
default = "Europe/Paris"
}
variable "locale" {
description = "Locale générée sur le système."
type = string
default = "fr_FR.UTF-8"
}
variable "ansible_user" {
description = "Compte unix créé par cloud-init, utilisé ensuite par Ansible."
type = string
default = "ansible"
}
variable "ssh_public_keys" {
description = "Clés publiques autorisées pour le compte d'administration. Aucun mot de passe n'est défini."
type = list(string)
validation {
condition = length(var.ssh_public_keys) > 0
error_message = "Au moins une clé publique est nécessaire : l'authentification par mot de passe est désactivée."
}
}
variable "ssh_allowed_cidrs" {
description = "Réseaux autorisés à joindre le port 22 (pare-feu local nftables)."
type = list(string)
default = ["10.0.4.0/24", "10.0.5.0/24"]
}
############################################
# Sortie pour Ansible
############################################
variable "ansible_inventory_path" {
description = "Chemin du fichier d'inventaire généré à la fin de l'apply."
type = string
default = "generated/inventory.yml"
}
+14
View File
@@ -0,0 +1,14 @@
terraform {
required_version = ">= 1.6.0"
required_providers {
proxmox = {
source = "bpg/proxmox"
version = ">= 0.69.0"
}
local = {
source = "hashicorp/local"
version = ">= 2.4.0"
}
}
}