For AI agents: the complete documentation index is available at https://docs.ovhcloud.com/pt/llms.txt, the full documentation bundle is available at https://docs.ovhcloud.com/pt/llms-full.txt, and this page is available as Markdown at https://docs.ovhcloud.com/pt/guides/hosted-private-cloud/cloud-platform/snc-cloud-platform-terraform.md.

Terraform guide

Ver como Markdown

Find out how to use the Terraform OpenStack and AWS providers on SNC Cloud Platform, from clouds.yaml authentication to S3-compatible storage

Objective

Practical guide to using the terraform-provider-openstack/openstack provider (and the aws provider reconfigured for S31-compatible Object Storage) on this platform. Target audience: DevOps/SRE already familiar with Terraform.

Overview

The platform exposes a standard OpenStack API (Keystone, Nova, Neutron, Cinder, Glance, Placement) reachable via the official terraform-provider-openstack/openstack Terraform provider, plus an S3-compatible Object Storage endpoint (Ceph RGW or equivalent) driven through the hashicorp/aws provider repointed at a custom endpoint. There is no dedicated S3-compatible Terraform provider: this is the standard pattern for this kind of platform.

ComponentTerraform providerUsage
Identity / Compute / Network / Block Storage / Imageterraform-provider-openstack/openstackAll standard OpenStack resources
S3-compatible Object Storagehashicorp/aws (reconfigured)Buckets, objects
SSH key / self-signed TLS certificatehashicorp/tlsKey generation, no external API call

Requirements

  • Terraform >= 1.5 (>= 1.10 for the remote backend with native S3 locking, see the "Recommended workflow" section).
  • A clouds.yaml file with an application credential (not a classic login/password pair).
  • For Object Storage: an S3-compatible access key / secret key pair, separate from clouds.yaml.

The application credential is retrieved from the manager, via the Get your Application Credentials link in the API Access & Documentation section of the dashboard:

Manager dashboard showing the service tiles and the Get your Application Credentials link

Then under API access > Application credentials, where you can download a ready-to-use clouds.yaml directly or create a new credential:

Manager API access page with the Download clouds.yaml button and existing application credentials

clouds:
  openstack:
    auth:
      auth_url: https://auth.<region>.cloud.snc.ovh.net/v3
      application_credential_id: "..."
      application_credential_secret: "..."
    region_name: "podX.rbx"
    interface: "public"
    identity_api_version: 3
    auth_type: "v3applicationcredential"

Instructions

Authentication & provider configuration

OpenStack

The provider reads clouds.yaml through the standard openstacksdk environment variables — no secret hardcoded in .tf files:

provider "openstack" {
  cloud = "openstack" # section name inside clouds.yaml
}
export OS_CLOUD=openstack
export OS_CLIENT_CONFIG_FILE=/path/to/clouds.yaml   # if the file is not in a standard location

OS_CLIENT_CONFIG_FILE is needed whenever clouds.yaml does not live in the current directory, ~/.config/openstack/, or /etc/openstack/ — which is usually the case in a versioned repository, where the file is kept at a dedicated path so it can easily be excluded from version control.

S3-compatible Object Storage

provider "aws" {
  region                      = "us-east-1" # arbitrary value, not checked by the RGW
  access_key                  = var.s3_access_key
  secret_key                  = var.s3_secret_key
  skip_credentials_validation = true
  skip_region_validation      = true
  skip_requesting_account_id  = true

  endpoints {
    s3 = var.s3_endpoint_url
  }
}

The 3 skip_* flags are mandatory: the aws provider defaults to validating the region and account against real AWS IAM/STS, which do not exist here. Without them, terraform plan fails before it even touches the bucket.

s3_access_key, s3_secret_key and s3_endpoint_url are retrieved from the manager, on the Object Storage > Connect page:

Manager Object Storage page, Connection Details with Access Key, Secret Key and regional Endpoint

The secret key is only shown once, when the access key is created — if it is lost, delete the access key and create a new one.

Platform specifics to know before you start

These platform-specific points are not covered by the generic provider documentation.

1. No floating IP / Neutron router

The floating IP service will be available starting with the platform's major release #2. In the meantime, publicly exposed instances get their IP via a second network interface attached directly to the shared external network Ext-Net, which hands them a fixed public IP:

data "openstack_networking_network_v2" "ext_net" {
  name = "Ext-Net"
}

resource "openstack_compute_instance_v2" "example" {
  # ...
  network {
    uuid = openstack_networking_network_v2.private.id
  }

  network {
    uuid           = data.openstack_networking_network_v2.ext_net.id
    access_network = true # marks this interface as the source of access_ip_v4
  }
}

access_network = true explicitly tells the provider which interface should feed the computed access_ip_v4 attribute — without it, the choice between multiple interfaces is not guaranteed.

This Ext-Net interface, created implicitly here by the provider, has a behaviour worth knowing: the underlying Neutron port is automatically deleted if the instance is destroyed, releasing the public IP back into the pool (it is not guaranteed to be reassigned to the next instance). The Managing public IPs guide details how to create this port independently of the instance so the same public address can be kept across instances.

2. Every flavor has a 0 GB root disk

Direct consequence: booting an instance from image_id alone fails with Only volume-backed servers are allowed for flavors with zero disk. A block_device with destination_type = "volume" is always required, creating a boot Cinder volume from the image:

resource "openstack_compute_instance_v2" "example" {
  # ...
  block_device {
    uuid                  = data.openstack_images_image_v2.os.id
    source_type           = "image"
    destination_type      = "volume"
    volume_size           = 20 # must exceed the image's virtual size, not its on-disk footprint
    boot_index            = 0
    delete_on_termination = true
  }
}
Warning

volume_size must be strictly larger than the image's virtual size (Glance's virtual_size), not its compressed on-disk size. An image that weighs a few hundred MB compressed can have a virtual size of 15-20 GB once decompressed — undersizing the volume produces Image virtual size is XGB and doesn't fit in a volume of size YGB.

3. Reusing existing resources via data sources

System images (Debian 13, AlmaLinux 10, etc.) and the external network already exist on the project — do not recreate them:

data "openstack_images_image_v2" "os" {
  name        = "Debian 13"
  most_recent = true
}

Resource patterns

Access-restricted security group

Recommended pattern to scope exposure to known IP ranges (internal network, VPN) rather than 0.0.0.0/0, without hand-duplicating one rule per port × CIDR:

locals {
  allowed_cidrs = [
    "203.0.113.0/24",
    "198.51.100.0/26",
  ]

  allowed_ports = {
    ssh   = 22
    http  = 80
    https = 443
  }

  secgroup_rules = {
    for pair in setproduct(keys(local.allowed_ports), local.allowed_cidrs) :
    "${pair[0]}-${pair[1]}" => {
      port = local.allowed_ports[pair[0]]
      cidr = pair[1]
    }
  }
}

resource "openstack_networking_secgroup_rule_v2" "allowed" {
  for_each = local.secgroup_rules

  security_group_id = openstack_networking_secgroup_v2.this.id
  direction         = "ingress"
  ethertype         = "IPv4"
  protocol          = "tcp"
  port_range_min    = each.value.port
  port_range_max    = each.value.port
  remote_ip_prefix  = each.value.cidr
}

setproduct() builds the port × CIDR Cartesian product; the for_each key ("${port}-${cidr}") keeps Terraform state stable even if list ordering changes — essential to avoid cascading destroy/create on a mere reordering.

Compute: SSH key generated by Terraform

No need to manage a separate keypair — the tls provider generates one, stored in state (treat it as a secret):

resource "tls_private_key" "this" {
  algorithm = "RSA"
  rsa_bits  = 4096
}

resource "openstack_compute_keypair_v2" "this" {
  name       = "${var.prefix}-keypair"
  public_key = tls_private_key.this.public_key_openssh
}

output "ssh_private_key" {
  value     = tls_private_key.this.private_key_pem
  sensitive = true
}

S3-compatible Object Storage

Once the aws provider is repointed (see above), standard resources work as expected:

resource "aws_s3_bucket" "data" {
  bucket = "${var.prefix}-data"
}

Secrets and sensitive data

  • Declare any variable holding a secret (application credential, S3 key, password) with sensitive = true — Terraform hides its value in plan/apply logs, but it still ends up in plaintext in the state. An encrypted remote backend (or at minimum a local state excluded from version control) becomes mandatory as soon as the project goes beyond individual use.
  • A terraform.tfvars file holding real values must be excluded from version control (.gitignore), same as clouds.yaml.
Warning

bcrypt() pitfall: Terraform's native bcrypt(string) function generates a random salt on every evaluation. Calling it directly inside a resource (e.g. a Caddyfile rendered via templatefile() and embedded in user_data) means the hash changes on every plan, forcing an instance replacement every single run — even with no real change. Precompute the hash once (outside Terraform, or via an implementation script) and pass it as a static value through a sensitive variable to avoid this.

Application bootstrap via user_data / cloud-init

Pattern to embed a complete application (code, configuration, systemd service) declaratively, without a separate SSH provisioner:

locals {
  cloud_init = templatefile("${path.module}/cloud-init.tftpl", {
    app_py_b64 = base64encode(file("${path.module}/../app/app.py"))
    # ...
  })
}

resource "openstack_compute_instance_v2" "this" {
  user_data = local.cloud_init
  # ...
}

The cloud-init template uses write_files (with encoding: b64 for binary/encoded content) and runcmd to install and start services.

Warning

Ordering pitfall: if a custom configuration file overwrites a package's conffile before that package is installed (e.g. writing /etc/caddy/Caddyfile before apt-get install caddy), dpkg detects a conflict and shows an interactive prompt. Under cloud-init there is no TTY, so dpkg fails silently, which can prevent the package's postinst script from completing correctly (e.g. a system user is never created). Always install the package before overwriting its config files, copying the custom file in via a runcmd step after installation rather than through write_files directly at the final path.

export OS_CLOUD=openstack
export OS_CLIENT_CONFIG_FILE=/path/to/clouds.yaml

terraform fmt -recursive
terraform validate
terraform plan -input=false
terraform apply -input=false   # review the plan before confirming
  • terraform validate makes no network calls: run it before every plan.
  • Always review a plan before apply, especially the count of destroyed resources — an innocuous-looking change inside a templatefile() (e.g. user_data content) forces a full instance replacement.
  • Local state (terraform.tfstate) is fine for individual/exploratory use. For team use, migrate to a remote backend on this same S3-compatible endpoint, as described in the Using OVHcloud Object Storage as Terraform Backend to store your Terraform state guide, with native locking enabled:
# backend.tf — same bucket/key and S3 credentials as the resource provider
terraform {
    backend "s3" {
      bucket = "terraform-state"
      key    = "terraform.tfstate"
      region = "eu-west-rbx-snc"
      # any activated storage region
      endpoints = {
        s3 = "https://<s3-endpoint>/"
      }
      skip_credentials_validation = true
      skip_region_validation      = true
      skip_requesting_account_id  = true
      skip_s3_checksum            = true

      # The following fields should be added if your Object Storage user credentials are not
      # already configured in files ~/.aws/credentials, ~/.aws/config or in
      # environment variables.
      access_key                  = "s3 user access key"
      secret_key                  = "s3 user secret key"

      # Native S3 state lock via conditional writes (requires Terraform >= 1.10)
      use_lockfile = true
    }
}

Known pitfalls (troubleshooting)

SymptomCauseFix
Neither a boot device, image ID, or image name were able to be determinedMalformed or missing block_device while no image_id/image_name is set on the resourceCheck the block_device block syntax, or pass image_id directly if the platform allows it
Only volume-backed servers are allowed for flavors with zero diskZero-root-disk flavor, direct image boot attemptedUse a block_device with destination_type = "volume"
Image virtual size is XGB and doesn't fit in a volume of size YGBvolume_size smaller than the image's actual virtual sizeIncrease volume_size with a comfortable margin above the virtual size reported by Glance
plan wants to replace the instance on every run, with no apparent changeA call to bcrypt() (or any non-deterministic function) evaluated into a value feeding user_dataPrecompute the value outside Terraform and supply it as a static variable
terraform plan fails immediately on the aws provider with account/region errorsMissing skip_credentials_validation/skip_region_validation/skip_requesting_account_idAdd them to the provider "aws" block
A service fails to start with a missing-system-user error, after an APT package is installed via cloud-initPackage conffile overwritten before install, dpkg stuck on an interactive promptInstall the package before overwriting its config files (copy via runcmd after install, not via write_files at the final path)

Go further

Official documentation: terraform-provider-openstack, AWS provider, TLS provider.

For training or technical assistance implementing our solutions, contact your sales representative or visit our Professional Services page to request a quote and have your project analysed by our experts.

Join our community of users.

1: S3 is a trademark of Amazon Technologies, Inc. OVHcloud's service is not sponsored by, endorsed by, or otherwise affiliated with Amazon Technologies, Inc.

Esta página foi útil?