Déployer une infra Scaleway avec Terraform

Déploie un serveur Scaleway avec Terraform de zéro : projet, clé SSH, security group, instance et cloud-init. Guide pas à pas avec estimation des coûts.

Publié le Mis à jour le , mis à jour le 15 min de lecture

Dans mon article précédentInfrastructure Scaleway avec Terraform · partie 19 minIntroduction à Terraform avec DockerApprendre Terraform sans compte cloud, avec le provider Docker : un premier container, puis PostgreSQL et Adminer, les variables et le cycle plan/apply.Lire l'articleNouvel ongletUn bandeau t'y proposera de reprendre ici., on a découvert Terraform avec Docker en local. C'était pratique pour apprendre sans payer, mais à un moment il faut un vrai serveur. On va en créer un sur Scaleway, entièrement décrit en Terraform, jusqu'à la clé SSH pour s'y connecter.

L'article s'adresse à quelqu'un qui a déjà lancé un terraform apply et qui veut passer à un vrai cloud. Compte une douzaine d'euros par mois si tu laisses tourner le serveur, quelques centimes si tu le détruis à la fin.

Pourquoi Scaleway

Un cloud provider français, avec des datacenters à Paris. Pour un projet perso ou une petite prod, ça veut dire une latence faible depuis la France et des données hébergées dans l'Union européenne. Une instance DEV1-S (2 vCPU, 2 Go de RAM) coûte environ 6,50 € HT par mois, ce qui suffit largement pour héberger quelques containers. Et le provider Terraform est bien maintenu, ce qui n'est pas le cas chez tous les hébergeurs de cette taille.

Ce qu'on va construire

À la fin de cet article, tu auras déployé :

  • Un projet Scaleway pour isoler tes ressources
  • Une clé SSH générée automatiquement par Terraform
  • Un Security Group avec règles firewall
  • Une instance DEV1-S avec Ubuntu 24.04, Docker déjà installé
flowchart TD
    P[Projet Scaleway] --> K[Clé SSH]
    P --> SG[Security Group]
    P --> IP[IP publique]
    K --> S[Instance DEV1-S]
    SG --> S
    IP --> S
    CI[cloud-init] --> S

Prérequis

  • Terraform 1.4 ou plus récent
  • Un compte Scaleway (console.scaleway.com)
  • Tes credentials API Scaleway (Access Key + Secret Key)

Pour créer tes credentials, rends-toi dans la console Scaleway : IAM > API Keys > Generate API Key. Note bien la secret key au moment de la création, elle ne sera plus affichée ensuite.

Architecture des fichiers

infra/
├── main.tf           # Ressources principales
├── variables.tf      # Définition des variables
├── outputs.tf        # Valeurs de sortie
├── versions.tf       # Configuration Terraform et providers
└── terraform.tfvars  # Valeurs des variables (non committé)

Configuration du provider

versions.tf

On commence par dire à Terraform quels providers on utilise et dans quelles versions. Bloquer les versions majeures évite les mauvaises surprises au prochain terraform init :

versions.tf
terraform {
  required_version = ">= 1.4"

  required_providers {
    scaleway = {
      source  = "scaleway/scaleway"
      version = "~> 2.0"
    }
    tls = {
      source  = "hashicorp/tls"
      version = "~> 4.0"
    }
    local = {
      source  = "hashicorp/local"
      version = "~> 2.0"
    }
    http = {
      source  = "hashicorp/http"
      version = "~> 3.0"
    }
  }
}

Quatre providers, dont trois qui ne touchent pas au cloud : scaleway/scaleway pour les ressources, hashicorp/tls pour générer la clé SSH, hashicorp/local pour l'écrire sur le disque, et hashicorp/http pour récupérer ton IP publique. On verra plus bas pourquoi on en a besoin.

Le required_version vaut >= 1.4 parce qu'on utilise plus loin la ressource terraform_data, arrivée avec Terraform 1.4.

variables.tf

Les variables, c'est tout ce qui change d'un déploiement à l'autre : credentials, nom du projet, taille de l'instance.

variables.tf
# Credentials Scaleway
variable "scw_access_key" {
  description = "Scaleway Access Key"
  type        = string
  sensitive   = true
}

variable "scw_secret_key" {
  description = "Scaleway Secret Key"
  type        = string
  sensitive   = true
}

variable "scw_organization_id" {
  description = "Scaleway Organization ID"
  type        = string
}

# Configuration
variable "project_name" {
  description = "Nom du projet Scaleway"
  type        = string
  default     = "mon-projet"
}

variable "region" {
  description = "Scaleway region"
  type        = string
  default     = "fr-par"
}

variable "zone" {
  description = "Scaleway zone"
  type        = string
  default     = "fr-par-1"
}

variable "instance_type" {
  description = "Type d'instance (DEV1-S, DEV1-M, PLAY2-NANO...)"
  type        = string
  default     = "DEV1-S"
}

L'attribut sensitive = true empêche Terraform d'afficher ces valeurs dans la sortie du plan et de l'apply. Elles restent en clair dans le state, par contre. Garde ça en tête pour la suite.

Un mot sur region et zone, parce que la distinction n'est pas évidente au début. Chez Scaleway, une région (fr-par) contient plusieurs zones (fr-par-1, fr-par-2, fr-par-3). Les ressources sont créées dans une zone précise.

terraform.tfvars

Crée ce fichier pour stocker tes valeurs :

terraform.tfvars
scw_access_key      = "SCWXXXXXXXXXXXXXXXXX"
scw_secret_key      = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
scw_organization_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
project_name        = "mon-super-projet"

Attention

Ce fichier contient tes secrets, il ne doit jamais finir dans git. Ajoute terraform.tfvars, *.auto.tfvars et *_ed25519 à ton .gitignore tout de suite, avant le premier commit. C'est le genre de truc qu'on remet à plus tard et qu'on regrette.

Créer le projet et configurer le provider

Tout ce qui suit va dans main.tf, bloc après bloc.

main.tf
# Configuration du provider
provider "scaleway" {
  access_key      = var.scw_access_key
  secret_key      = var.scw_secret_key
  organization_id = var.scw_organization_id
  region          = var.region
  zone            = var.zone
}

# Créer un projet pour isoler les ressources
resource "scaleway_account_project" "main" {
  name        = var.project_name
  description = "Projet géré par Terraform"
}

Un projet Scaleway, c'est un compartiment : ses ressources, ses permissions, sa facture. Un projet par environnement (dev, staging, prod) te permet de tout détruire d'un côté sans risquer de toucher à l'autre.

Générer une clé SSH automatiquement

Tu pourrais créer une clé SSH à la main et coller la partie publique dans la console. Mais si Terraform peut la générer, l'enregistrer chez Scaleway et l'écrire sur ton disque, autant lui laisser faire. Tout reste dans le même apply :

main.tf
# Générer une paire de clés ED25519
resource "tls_private_key" "ssh" {
  algorithm = "ED25519"
}

# Enregistrer la clé publique dans Scaleway
resource "scaleway_iam_ssh_key" "main" {
  name       = "${var.project_name}-terraform"
  public_key = trimspace(tls_private_key.ssh.public_key_openssh)
  project_id = scaleway_account_project.main.id
}

# Sauvegarder la clé privée localement
resource "local_sensitive_file" "ssh_private_key" {
  content         = tls_private_key.ssh.private_key_openssh
  filename        = "${path.module}/${var.project_name}_ed25519"
  file_permission = "0600"
}

# Sauvegarder la clé publique localement
resource "local_file" "ssh_public_key" {
  content  = tls_private_key.ssh.public_key_openssh
  filename = "${path.module}/${var.project_name}_ed25519.pub"
}

ED25519 plutôt que RSA : des clés plus courtes, plus rapides, et au moins aussi sûres. Le trimspace() enlève le retour à la ligne final que public_key_openssh ajoute, pour ne pas envoyer une clé avec un caractère parasite à Scaleway. Et file_permission = "0600" n'est pas optionnel : SSH refuse d'utiliser une clé privée lisible par d'autres utilisateurs. path.module désigne simplement le dossier où sont tes fichiers Terraform.

La clé privée passe par local_sensitive_file plutôt que local_file : même comportement, mais son contenu n'apparaît jamais dans la sortie du plan.

Attention

La clé privée se retrouve aussi dans le state, en clair. Si tu partages ce state avec une équipe, tout le monde peut se connecter au serveur.

Configurer le Security Group

Un Security Group, c'est le firewall de ton instance, géré côté Scaleway avant même que le trafic n'atteigne la machine. On bloque tout en entrée, on autorise SSH uniquement depuis ton IP, et on ouvre les ports 80 et 443 pour le web.

main.tf
# Récupérer notre IP publique actuelle
data "http" "my_ip" {
  url = "https://ipv4.icanhazip.com"
}

locals {
  my_ip = "${chomp(data.http.my_ip.response_body)}/32"
}

# Créer le Security Group
resource "scaleway_instance_security_group" "main" {
  project_id              = scaleway_account_project.main.id
  name                    = "${var.project_name}-sg"
  description             = "Security group pour ${var.project_name}"
  inbound_default_policy  = "drop"   # Bloquer tout par défaut
  outbound_default_policy = "accept" # Autoriser les sorties

  # SSH - restreint à notre IP uniquement
  inbound_rule {
    action   = "accept"
    port     = 22
    protocol = "TCP"
    ip_range = local.my_ip
  }

  # HTTP - ouvert à tous
  inbound_rule {
    action   = "accept"
    port     = 80
    protocol = "TCP"
  }

  # HTTPS - ouvert à tous
  inbound_rule {
    action   = "accept"
    port     = 443
    protocol = "TCP"
  }
}

C'est là qu'intervient le provider http : le data source appelle un service qui renvoie ton IP publique, chomp() enlève le retour à la ligne, et le /32 en fait un CIDR d'une seule adresse. Résultat, le port 22 n'est ouvert que pour toi.

Le revers : si ton IP change (box qui redémarre, 4G, VPN), le prochain terraform apply mettra la règle à jour, mais en attendant tu es dehors. Pour une machine que tu gères seul, je trouve que ça vaut le coup. Pour une équipe, prévois une règle par personne ou un bastion.

Le reste suit le principe du deny by default : on bloque tout, puis on ouvre explicitement ce dont on a besoin.

Déployer l'instance

Réserver une IP publique

main.tf
resource "scaleway_instance_ip" "main" {
  project_id = scaleway_account_project.main.id
}

On réserve l'IP comme une ressource à part, et on l'attache ensuite à l'instance. Comme ça, si tu détruis et recrées le serveur, l'IP reste la même et ton DNS ne bouge pas. Ça arrive à chaque modification du cloud-init, on le verra juste après.

Créer l'instance

main.tf
resource "scaleway_instance_server" "main" {
  project_id = scaleway_account_project.main.id
  name       = "${var.project_name}-server"
  type       = var.instance_type
  image      = "ubuntu_noble"

  # Attacher l'IP et le Security Group
  ip_id             = scaleway_instance_ip.main.id
  security_group_id = scaleway_instance_security_group.main.id

  tags = ["terraform", "production"]

  # La clé SSH doit exister avant le premier démarrage
  depends_on = [scaleway_iam_ssh_key.main]
}

Le depends_on mérite un mot. Scaleway installe les clés SSH du projet sur l'instance au moment où elle démarre. Or rien dans ce bloc ne référence la clé, donc Terraform pourrait créer le serveur en parallèle, avant qu'elle soit enregistrée. On lui impose l'ordre à la main.

Pour choisir le type d'instance, voici quelques repères. Les prix sont ceux du catalogue Scaleway en novembre 2026 pour fr-par-1, hors taxes, pour un mois complet de 730 heures, sans le stockage ni l'IP :

TypevCPURAMPrix/mois
DEV1-S22 Go~6,50 €
DEV1-M34 Go~14,70 €
DEV1-L48 Go~31,30 €
PLAY2-NANO24 Go~20,10 €
GP1-XS416 Go~67,80 €

Les DEV1 sont une gamme de première génération, toujours en vente et moins chère que les PLAY2 qui lui ont succédé. La facturation se fait à l'heure : un serveur détruit au bout d'un après-midi coûte quelques centimes.

Côté images, tu as ubuntu_noble (24.04), ubuntu_resolute (26.04), ubuntu_jammy (22.04), debian_trixie, debian_bookworm, entre autres.

Provisionner avec Cloud-init

À ce stade tu as un serveur vide. Cloud-init est l'outil, présent sur les images cloud, qui configure la machine au premier démarrage : installer des packages, créer des fichiers, lancer des commandes. L'intérêt, c'est que tout est décrit dans ton dépôt. Tu ne te connectes jamais en SSH pour installer Docker à la main.

Version simple (inline)

main.tf
resource "scaleway_instance_server" "main" {
  # ... configuration précédente ...

  user_data = {
    cloud-init = <<-EOF
      #cloud-config
      package_update: true
      package_upgrade: true

      packages:
        - ca-certificates
        - curl
        - git
        - htop
        - docker.io
        - docker-compose-v2

      runcmd:
        - systemctl enable --now docker
    EOF
  }
}

Les paquets docker.io et docker-compose-v2 viennent des dépôts d'Ubuntu. Le second apporte la commande docker compose, dont on aura besoin dans la suite de la série.

Attention

Cloud-init ne tourne qu'au premier démarrage. Si tu modifies le user_data d'une instance qui existe déjà, rien ne se passe. C'est le piège classique, et c'est pour ça que la version suivante force la recréation de l'instance quand le contenu change.

Version avec fichier template

Dès que le cloud-init dépasse quelques lignes, un fichier séparé est plus lisible, et tu peux y injecter des variables :

main.tf
locals {
  cloud_init_content = templatefile("${path.module}/templates/cloud-init.yml", {
    project_name = var.project_name
  })
}

resource "scaleway_instance_server" "main" {
  # ... configuration précédente ...

  user_data = {
    cloud-init = local.cloud_init_content
  }

  # Recréer l'instance si cloud-init change
  lifecycle {
    replace_triggered_by = [terraform_data.cloud_init_hash]
  }
}

# Hash du cloud-init pour détecter les changements
resource "terraform_data" "cloud_init_hash" {
  input = md5(local.cloud_init_content)
}

Et dans templates/cloud-init.yml :

templates/cloud-init.yml
#cloud-config
package_update: true
package_upgrade: true

packages:
  - ca-certificates
  - curl
  - git
  - htop
  - docker.io
  - docker-compose-v2

runcmd:
  - systemctl enable --now docker
  - echo "Provisioning complete for ${project_name}"

La fonction templatefile() remplace les ${...} du YAML par les valeurs que tu lui passes. Le terraform_data avec le hash md5, lui, est le déclencheur : si le contenu du cloud-init change, le hash change, et replace_triggered_by recrée l'instance. Ça détruit la machine et son disque, donc réfléchis avant de modifier le template en prod.

Outputs

Les outputs, c'est ce que Terraform t'affiche à la fin de l'apply. Je mets toujours la commande SSH complète dedans, ça évite d'aller chercher l'IP et le chemin de la clé :

outputs.tf
output "project_id" {
  description = "ID du projet Scaleway"
  value       = scaleway_account_project.main.id
}

output "instance_public_ip" {
  description = "IP publique de l'instance"
  value       = scaleway_instance_ip.main.address
}

output "ssh_command" {
  description = "Commande SSH pour se connecter"
  value       = "ssh -i ${local_sensitive_file.ssh_private_key.filename} root@${scaleway_instance_ip.main.address}"
}

Après terraform apply, tu peux afficher ces valeurs avec :

Terminal
terraform output

Déployer l'infrastructure

Initialiser Terraform

Terminal
$ terraform init

Initializing the backend...
Initializing provider plugins...
- Finding scaleway/scaleway versions matching "~> 2.0"...
- Installing scaleway/scaleway v2.x.x...

Terraform has been successfully initialized!

Prévisualiser les changements

Terminal
$ terraform plan

Plan: 9 to add, 0 to change, 0 to destroy.

Neuf ressources avec la version template : le projet, la paire de clés, son enregistrement chez Scaleway, les deux fichiers locaux, le Security Group, l'IP, le hash du cloud-init et l'instance.

Prends le temps de lire le plan. Un destroy qui se glisse dans la liste quand tu pensais juste modifier un tag, ça arrive, et c'est le seul moment où tu peux l'attraper.

Appliquer

Terminal
$ terraform apply

Do you want to perform these actions?
  Enter a value: yes

Apply complete! Resources: 9 added, 0 changed, 0 destroyed.

Outputs:

instance_public_ip = "51.15.xxx.xxx"
ssh_command = "ssh -i ./mon-projet_ed25519 root@51.15.xxx.xxx"

Se connecter à l'instance

Terminal
$ ssh -i ./mon-projet_ed25519 root@51.15.xxx.xxx

Welcome to Ubuntu 24.04.x LTS
root@mon-projet-server:~#

L'apply rend la main dès que l'instance a démarré, alors que cloud-init travaille encore : la mise à jour des paquets et l'installation de Docker prennent quelques minutes. Pour savoir où il en est, une fois connecté :

Terminal
$ cloud-init status --wait
status: done

$ docker compose version

Partager le state avec HCP Terraform

Tant que tu es seul sur ton poste, le state en local suffit. Dès que tu travailles à deux ou depuis une CI, il faut le partager, et le mettre dans git est une mauvaise idée (il contient des secrets, on l'a vu). HCP Terraform, le service qui s'appelait Terraform Cloud jusqu'en 2024, règle ça gratuitement jusqu'à 500 ressources gérées :

versions.tf
terraform {
  cloud {
    organization = "mon-organisation"

    workspaces {
      name = "mon-projet-prod"
    }
  }

  # ... required_providers ...
}

Le state ne traîne plus sur un disque local : tout le monde voit le même état, un verrou empêche deux apply en même temps, et tu gardes l'historique des versions.

Connecte-toi avec :

Terminal
$ terraform login
$ terraform init

Quelques habitudes qui aident

1. Structure des fichiers

Un seul main.tf suffit pour cet article. Dès que ça grossit, découper par thème rend les diffs plus lisibles :

infra/
├── main.tf           # Provider et ressources principales
├── network.tf        # Security groups, VPC
├── compute.tf        # Instances
├── variables.tf
├── outputs.tf
├── versions.tf
└── templates/
    └── cloud-init.yml

2. Nommage cohérent

Préfixe tes ressources avec le nom du projet. Dans la console Scaleway, quand tu as trois projets, un serveur qui s'appelle server ne t'aide pas :

Terraform
resource "scaleway_instance_server" "main" {
  name = "${var.project_name}-server"
}

3. Utilise des tags

Les tags facilitent le filtrage et la gestion des coûts :

Terraform
tags = ["terraform", "production", var.project_name]

4. Ne jamais modifier le state manuellement

Si une ressource existe déjà chez Scaleway et que tu veux que Terraform la gère, passe par import plutôt que d'éditer le fichier :

Terminal
terraform import scaleway_instance_server.main fr-par-1/instance-id

5. Détruire proprement

L'avantage d'avoir tout en Terraform, c'est que tout disparaît aussi proprement que c'est arrivé. Pas d'IP oubliée qui continue à être facturée :

Terminal
$ terraform destroy

Pour supprimer une ressource spécifique :

Terminal
$ terraform destroy -target=scaleway_instance_server.main

Ce que ça coûte

Scaleway facture séparément l'instance, son IPv4 publique et son stockage. Pour l'infrastructure décrite dans cet article, aux tarifs de novembre 2026, hors taxes :

RessourceCoût mensuel
Instance DEV1-S (0,009 €/h)~6,50 €
IPv4 flexible (0,005 €/h)~3,65 €
Stockage, pour 20 Go, selon le type de volume~0,70 à 1,90 €
Total~11 à 12 €/mois

L'IPv4 n'est pas gratuite, même attachée à une instance. Elle est facturée de sa réservation à sa suppression, que le serveur tourne ou non. Même chose pour le volume. Les tarifs bougent, vérifie-les sur la page de prix de Scaleway avant de te lancer.

Attention

Éteindre l'instance n'arrête pas toute la facture : l'IP et le volume continuent d'être comptés. C'est aussi ce qui se passe si tu détruis seulement l'instance avec -target et que tu laisses l'IP derrière. Pour ne plus rien payer, il faut un terraform destroy complet.

Pour aller plus loin

Tu as maintenant un serveur que tu peux recréer à l'identique en une commande. Fini la machine configurée à la main un soir, dont personne ne sait plus ce qu'il y a dessus. Il lui manque encore quelque chose à servir : dans la partie suivante, on y déploie une application avec Docker Compose, Traefik et Let's EncryptInfrastructure Scaleway avec Terraform · partie 316 minDéployer avec Compose, Traefik et Let's EncryptOn installe Traefik en reverse proxy avec Docker Compose, des certificats Let's Encrypt renouvelés tout seuls, puis une application et sa base de données.Lire l'articleNouvel ongletUn bandeau t'y proposera de reprendre ici, section 16..

Infrastructure Scaleway avec Terraform · partie 2 sur 3

La suite, partie 3Déployer avec Compose, Traefik et Let's Encrypt

On installe Traefik en reverse proxy avec Docker Compose, des certificats Let's Encrypt renouvelés tout seuls, puis une application et sa base de données.

16 min de lecture

Voir en Markdown

À lire ensuite

Infrastructure Scaleway avec Terraform · 1

Introduction à Terraform avec Docker

Apprendre Terraform sans compte cloud, avec le provider Docker : un premier container, puis PostgreSQL et Adminer, les variables et le cycle plan/apply.

Cité dans cet article

DevOps

Le DevOps n'est pas un métier

Le DevOps est une façon de travailler en équipe, née en 2009, et les offres d'emploi en ont fait un poste à pourvoir. D'où vient le mot, et ce que j'en retiens.

IA pour les développeurs · 2

Agents IA : boucle agentique et pattern ReAct

Construis un agent IA autonome en TypeScript avec Mistral : boucle agentique, pattern ReAct, planification, mémoire et garde-fous. Code complet inclus.

Alexandre Simonet, développeur fullstack.
J'écris ici sur les serveurs MCP, les agents et l'infra pour les faire tourner.