Skip to content

Modules Terraform - Compute

Introduction

Le module compute gère les instances, volumes, floating IPs et keypairs. Il s'intègre avec le module network pour créer des stacks applicatives complètes.

Prérequis

Points à apprendre

Architecture du module

graph TB
    subgraph "Module: compute"
        Instance["Instances<br/>openstack_compute_instance_v2<br/>VMs avec cloud-init"]
        Volume["Volumes<br/>openstack_blockstorage_volume_v3<br/>Stockage persistant"]
        Attach["Volume Attach<br/>openstack_compute_volume_attach_v2<br/>Attachement volumes"]
        FIP["Floating IPs<br/>openstack_networking_floatingip_v2<br/>IPs publiques"]
        Keypair["Keypair<br/>openstack_compute_keypair_v2<br/>Clé SSH"]
        ServerGroup["Server Group<br/>openstack_compute_servergroup_v2<br/>Anti-affinity"]
    end

    NetworkModule["Module: network<br/>Réseau et security groups"]
    Image["Glance Image<br/>Image de base"]
    Flavor["Flavor<br/>Configuration ressources"]

    Instance -->|Uses<br/>network_id, security_group_id| NetworkModule
    Instance -->|Boot from| Image
    Instance -->|Sized by| Flavor
    Instance -->|Auth| Keypair
    Attach -->|Attach to| Instance
    Attach -->|Attach| Volume
    FIP -->|Associate| Instance
    Instance -->|Member of| ServerGroup

Structure du module

modules/compute/
├── main.tf
├── variables.tf
├── outputs.tf
├── volumes.tf
├── cloud-init/
│   ├── base.yaml
│   └── web.yaml
└── README.md

Variables du module

# modules/compute/variables.tf

variable "name" {
  description = "Nom de base pour les instances"
  type        = string
}

variable "environment" {
  description = "Environnement"
  type        = string
  default     = "dev"
}

variable "instance_count" {
  description = "Nombre d'instances"
  type        = number
  default     = 1
}

variable "image_name" {
  description = "Nom de l'image"
  type        = string
  default     = "ubuntu-22.04"
}

variable "image_id" {
  description = "ID de l'image (prioritaire sur image_name)"
  type        = string
  default     = null
}

variable "flavor_name" {
  description = "Nom du flavor"
  type        = string
  default     = "m1.small"
}

variable "network_id" {
  description = "ID du réseau"
  type        = string
}

variable "security_group_ids" {
  description = "Liste des IDs de security groups"
  type        = list(string)
  default     = []
}

variable "security_group_names" {
  description = "Liste des noms de security groups"
  type        = list(string)
  default     = []
}

variable "keypair_name" {
  description = "Nom de la keypair existante"
  type        = string
  default     = null
}

variable "public_key" {
  description = "Clé publique SSH (crée une nouvelle keypair)"
  type        = string
  default     = null
}

variable "assign_floating_ip" {
  description = "Assigner une floating IP"
  type        = bool
  default     = false
}

variable "floating_ip_pool" {
  description = "Pool pour les floating IPs"
  type        = string
  default     = "external"
}

variable "availability_zone" {
  description = "Zone de disponibilité"
  type        = string
  default     = null
}

variable "user_data" {
  description = "Cloud-init user data"
  type        = string
  default     = null
}

variable "user_data_file" {
  description = "Chemin vers fichier cloud-init"
  type        = string
  default     = null
}

variable "volumes" {
  description = "Volumes à créer et attacher"
  type = list(object({
    size        = number
    type        = optional(string, "standard")
    mount_point = optional(string)
  }))
  default = []
}

variable "boot_volume_size" {
  description = "Taille du volume de boot (0 = boot depuis image)"
  type        = number
  default     = 0
}

variable "boot_volume_type" {
  description = "Type du volume de boot"
  type        = string
  default     = "standard"
}

variable "use_anti_affinity" {
  description = "Utiliser un server group anti-affinity"
  type        = bool
  default     = false
}

variable "metadata" {
  description = "Metadata pour les instances"
  type        = map(string)
  default     = {}
}

variable "tags" {
  description = "Tags pour les ressources"
  type        = map(string)
  default     = {}
}

Ressources principales

# modules/compute/main.tf

locals {
  name_prefix = "${var.environment}-${var.name}"
  common_tags = merge(var.tags, {
    environment = var.environment
    managed_by  = "terraform"
    module      = "compute"
  })

  user_data = coalesce(
    var.user_data,
    var.user_data_file != null ? file(var.user_data_file) : null,
    file("${path.module}/cloud-init/base.yaml")
  )

  security_groups = concat(
    var.security_group_names,
    [for id in var.security_group_ids : id]
  )
}

# Data source pour l'image
data "openstack_images_image_v2" "this" {
  count       = var.image_id == null ? 1 : 0
  name        = var.image_name
  most_recent = true
}

locals {
  image_id = coalesce(var.image_id, data.openstack_images_image_v2.this[0].id)
}

# Keypair (optionnel)
resource "openstack_compute_keypair_v2" "this" {
  count      = var.public_key != null ? 1 : 0
  name       = "${local.name_prefix}-keypair"
  public_key = var.public_key
}

locals {
  keypair_name = coalesce(
    var.keypair_name,
    var.public_key != null ? openstack_compute_keypair_v2.this[0].name : null
  )
}

# Server group pour anti-affinity
resource "openstack_compute_servergroup_v2" "this" {
  count    = var.use_anti_affinity ? 1 : 0
  name     = "${local.name_prefix}-servergroup"
  policies = ["anti-affinity"]
}

# Volume de boot (optionnel)
resource "openstack_blockstorage_volume_v3" "boot" {
  count             = var.boot_volume_size > 0 ? var.instance_count : 0
  name              = "${local.name_prefix}-boot-${count.index + 1}"
  size              = var.boot_volume_size
  volume_type       = var.boot_volume_type
  image_id          = local.image_id
  availability_zone = var.availability_zone
}

# Instance
resource "openstack_compute_instance_v2" "this" {
  count           = var.instance_count
  name            = "${local.name_prefix}-${count.index + 1}"
  flavor_name     = var.flavor_name
  key_pair        = local.keypair_name
  security_groups = local.security_groups
  availability_zone = var.availability_zone
  user_data       = local.user_data

  # Boot depuis image ou volume
  dynamic "block_device" {
    for_each = var.boot_volume_size > 0 ? [1] : []
    content {
      uuid                  = openstack_blockstorage_volume_v3.boot[count.index].id
      source_type           = "volume"
      destination_type      = "volume"
      boot_index            = 0
      delete_on_termination = false
    }
  }

  # Si pas de volume de boot, utiliser l'image
  image_id = var.boot_volume_size > 0 ? null : local.image_id

  network {
    uuid = var.network_id
  }

  # Server group anti-affinity
  dynamic "scheduler_hints" {
    for_each = var.use_anti_affinity ? [1] : []
    content {
      group = openstack_compute_servergroup_v2.this[0].id
    }
  }

  metadata = merge(local.common_tags, var.metadata, {
    instance_index = count.index + 1
  })

  lifecycle {
    ignore_changes = [user_data]
  }
}

# Floating IPs
resource "openstack_networking_floatingip_v2" "this" {
  count = var.assign_floating_ip ? var.instance_count : 0
  pool  = var.floating_ip_pool
}

resource "openstack_compute_floatingip_associate_v2" "this" {
  count       = var.assign_floating_ip ? var.instance_count : 0
  floating_ip = openstack_networking_floatingip_v2.this[count.index].address
  instance_id = openstack_compute_instance_v2.this[count.index].id
}

Gestion des volumes

# modules/compute/volumes.tf

# Volumes de données
resource "openstack_blockstorage_volume_v3" "data" {
  count             = var.instance_count * length(var.volumes)
  name              = "${local.name_prefix}-data-${floor(count.index / length(var.volumes)) + 1}-${count.index % length(var.volumes) + 1}"
  size              = var.volumes[count.index % length(var.volumes)].size
  volume_type       = var.volumes[count.index % length(var.volumes)].type
  availability_zone = var.availability_zone

  metadata = local.common_tags
}

# Attachement des volumes
resource "openstack_compute_volume_attach_v2" "data" {
  count       = var.instance_count * length(var.volumes)
  instance_id = openstack_compute_instance_v2.this[floor(count.index / length(var.volumes))].id
  volume_id   = openstack_blockstorage_volume_v3.data[count.index].id
}

Cloud-init templates

# modules/compute/cloud-init/base.yaml
#cloud-config
package_update: true
package_upgrade: true

packages:
  - curl
  - wget
  - vim
  - htop
  - net-tools

timezone: Europe/Paris

ntp:
  enabled: true
  servers:
    - ntp.ubuntu.com

users:
  - default
  - name: deploy
    groups: sudo
    shell: /bin/bash
    sudo: ['ALL=(ALL) NOPASSWD:ALL']

write_files:
  - path: /etc/motd
    content: |
      ====================================
      Managed by Terraform
      Environment: ${environment}
      ====================================

runcmd:
  - echo "Instance provisioned at $(date)" > /var/log/cloud-init-custom.log

final_message: "Instance ready after $UPTIME seconds"
# modules/compute/cloud-init/web.yaml
#cloud-config
package_update: true

packages:
  - nginx
  - certbot
  - python3-certbot-nginx

write_files:
  - path: /var/www/html/index.html
    content: |
      <!DOCTYPE html>
      <html>
      <head><title>Welcome</title></head>
      <body>
        <h1>Instance: ${hostname}</h1>
        <p>Environment: ${environment}</p>
      </body>
      </html>

runcmd:
  - systemctl enable nginx
  - systemctl start nginx

Outputs du module

# modules/compute/outputs.tf

output "instance_ids" {
  description = "IDs des instances créées"
  value       = openstack_compute_instance_v2.this[*].id
}

output "instance_names" {
  description = "Noms des instances"
  value       = openstack_compute_instance_v2.this[*].name
}

output "private_ips" {
  description = "Adresses IP privées"
  value       = openstack_compute_instance_v2.this[*].access_ip_v4
}

output "floating_ips" {
  description = "Floating IPs assignées"
  value       = var.assign_floating_ip ? openstack_networking_floatingip_v2.this[*].address : []
}

output "volume_ids" {
  description = "IDs des volumes de données"
  value       = openstack_blockstorage_volume_v3.data[*].id
}

output "keypair_name" {
  description = "Nom de la keypair utilisée"
  value       = local.keypair_name
}

output "server_group_id" {
  description = "ID du server group"
  value       = var.use_anti_affinity ? openstack_compute_servergroup_v2.this[0].id : null
}

# Output pour Ansible inventory
output "ansible_inventory" {
  description = "Inventaire Ansible généré"
  value = {
    for idx, instance in openstack_compute_instance_v2.this : instance.name => {
      ansible_host = var.assign_floating_ip ? openstack_networking_floatingip_v2.this[idx].address : instance.access_ip_v4
      private_ip   = instance.access_ip_v4
      instance_id  = instance.id
    }
  }
}

Utilisation du module

# environments/production/main.tf

# Réseau
module "app_network" {
  source      = "../../modules/network"
  name        = "webapp"
  environment = "production"
  cidr        = "10.10.0.0/24"
}

# Instances web (avec anti-affinity)
module "web_servers" {
  source = "../../modules/compute"

  name           = "web"
  environment    = "production"
  instance_count = 3

  image_name  = "ubuntu-22.04"
  flavor_name = "m1.medium"

  network_id           = module.app_network.network_id
  security_group_names = [module.app_network.security_group_name]

  public_key         = file("~/.ssh/id_rsa.pub")
  assign_floating_ip = true
  use_anti_affinity  = true

  user_data_file = "${path.module}/cloud-init/web.yaml"

  metadata = {
    role = "webserver"
  }

  tags = {
    project = "webapp"
  }
}

# Instance base de données
module "database" {
  source = "../../modules/compute"

  name           = "database"
  environment    = "production"
  instance_count = 1

  image_name  = "ubuntu-22.04"
  flavor_name = "m1.large"

  network_id           = module.app_network.network_id
  security_group_names = [module.app_network.database_security_group_id]

  public_key = file("~/.ssh/id_rsa.pub")

  # Volume de boot persistant
  boot_volume_size = 50
  boot_volume_type = "ssd"

  # Volumes de données
  volumes = [
    { size = 100, type = "ssd", mount_point = "/var/lib/postgresql" }
  ]

  metadata = {
    role = "database"
  }
}

# Outputs
output "web_floating_ips" {
  value = module.web_servers.floating_ips
}

output "database_private_ip" {
  value = module.database.private_ips[0]
}

output "ansible_inventory" {
  value = merge(
    module.web_servers.ansible_inventory,
    module.database.ansible_inventory
  )
}

Diagramme d'infrastructure

graph TB
    subgraph Internet
        Users[Users]
    end

    subgraph "OpenStack Cloud"
        subgraph "External Network"
            FIP1["FIP 203.0.113.10"]
            FIP2["FIP 203.0.113.11"]
            FIP3["FIP 203.0.113.12"]
        end

        subgraph "Project Network (10.10.0.0/24)"
            subgraph "Web Tier (Anti-affinity)"
                Web1["web-1<br/>m1.medium<br/>10.10.0.10"]
                Web2["web-2<br/>m1.medium<br/>10.10.0.11"]
                Web3["web-3<br/>m1.medium<br/>10.10.0.12"]
            end

            subgraph "Database Tier"
                DB["database-1<br/>m1.large<br/>10.10.0.20<br/>+ 100GB SSD"]
            end
        end
    end

    Users -->|HTTPS<br/>443| FIP1
    FIP1 --> Web1
    FIP2 --> Web2
    FIP3 --> Web3
    Web1 -->|PostgreSQL<br/>5432| DB
    Web2 -->|PostgreSQL<br/>5432| DB
    Web3 -->|PostgreSQL<br/>5432| DB

Exemples pratiques

Génération d'inventaire Ansible

# Exporter l'inventaire depuis Terraform
terraform output -json ansible_inventory | jq -r '
to_entries | .[] | "\(.key) ansible_host=\(.value.ansible_host) private_ip=\(.value.private_ip)"
' > inventory.ini

Ressources

Checkpoint

  • Module compute fonctionnel
  • Instances créées avec cloud-init
  • Volumes attachés correctement
  • Floating IPs assignées
  • Anti-affinity testé
  • Inventaire Ansible généré