---
title: "How to deploy Scality RING on OPCP"
description: "Find out how to deploy a Scality RING storage cluster on OPCP bare metal with Terraform, from node preparation to verification"
url: https://docs.ovhcloud.com/it/guides/hosted-private-cloud/opcp/how-to-deploy-scality-ring
lang: it
lastUpdated: 2026-09-25
---
> For AI agents: the complete documentation index is available at https://docs.ovhcloud.com/it/llms.txt, the full documentation bundle is available at https://docs.ovhcloud.com/it/llms-full.txt.

# How to deploy Scality RING on OPCP

## Objective

[Scality RING](https://www.scality.com/products/ring/)
 is a software-defined storage platform that turns a set of servers into a single object (S31
) and/or file (NFS/CIFS) storage system, with data protection handled either by erasure coding (ARC) or replication (COS) instead of a hardware RAID controller.
:::info
Scality RING is a [**certified OPCP third-party solution**](https://docs.ovhcloud.com/it/guides/hosted-private-cloud/opcp/opcp-compatibility-matrix.md). This deployment procedure is written and maintained by Scality, not OVHcloud. For support with RING itself, contact [Scality support](https://support.scality.com).
:::

OVHcloud's **On-Prem Cloud Platform (OPCP)** Bare Metal Pool exposes physical servers through OpenStack: Nova hands each instance to **Ironic**, which PXE-boots a whole physical machine with the image you provide, rather than running a virtual machine on a hypervisor. There is no console access and, on the target production platform, no direct SSH path onto the nodes before they are deployed.

This guide walks through deploying a RING cluster on OPCP bare metal, end to end:

1. Preparing the bare-metal nodes (network bonding and, optionally, software RAID for the OS disk).
2. Provisioning the network ports and bare-metal instances with Terraform.
3. Letting the RING installer run itself, driven by `cloud-init`, since the nodes cannot be reached over SSH before they boot.
4. Verifying the deployment and reaching the RING supervisor UI.

:::info
This guide does **not** cover building the Scality RING supervisor and storage disk images, or obtaining a RING licence. It assumes you already have both. See [Requirements](#requirements) below for where to get them.
:::

## Requirements

- An OVHcloud OPCP project with enough Bare Metal Pool nodes in the Ironic `available` state for your target topology. RING's minimum supported cluster on this platform is **1 supervisor node and 3 storage nodes**. The 3 storage nodes must be either **High Capacity Storage** catalogue models (write cache not available yet) or **High Performance Storage** models.

- An OpenStack **application credential** for that project, usable by both the OpenStack CLI and Terraform. See the _Creating an Application Credential from Horizon_ step of OVHcloud's [How to use Terraform](https://docs.ovhcloud.com/it/guides/hosted-private-cloud/opcp/use-terraform.md) guide for OPCP.

- The following tools installed on the workstation you will run the deployment from:
  - the [OpenStack CLI](https://docs.openstack.org/python-openstackclient/latest/) (`openstack`), to upload images (step 2), set up LACP/RAID (step 3) and run the verification commands (step 8);
  - [Terraform](https://developer.hashicorp.com/terraform) (or [OpenTofu](https://opentofu.org/)) with the [`terraform-provider-openstack`](https://registry.terraform.io/providers/terraform-provider-openstack/openstack) provider, to provision the network ports and bare-metal instances;
  - [`jq`](https://jqlang.org/), to parse the JSON output of the port group check in step 8;
  - an SSH client, useful if you need to reach the supervisor directly rather than only through its web UI.

- A Scality RING **supervisor** qcow2 image and one or more **storage** qcow2 images, based on Rocky Linux or RHEL — the only operating systems RING supports on OPCP — ready to upload to Glance. The supervisor image is expected to already carry the RING installer bundle, since the supervisor has no guaranteed way to fetch it after boot on this platform.

  :::info
  You do not build these images yourself: request the supervisor and storage qcow2 images (and the RING installer bundle, if not already embedded in the supervisor image) from Scality, through your account team or [Scality support](https://support.scality.com). Building or customising RING images is not covered by this guide — see the [Scality documentation portal](https://documentation.scality.com) for that procedure, or OVHcloud's guide on building a [custom CentOS Stream image](https://docs.ovhcloud.com/it/guides/hosted-private-cloud/opcp/how-to-create-centos-image.md) for background on the underlying Ironic/Nova image requirements.
  :::

- A valid Scality RING **licence**. RING will install and run without one only in a limited, time-boxed evaluation state; request a licence through [Scality support](https://support.scality.com) before going into production.

- A RING sizing-tool export (a `cluster.csv` describing your node roles, disk layout and RING sizing parameters), typically provided by your Scality account team or support contact.

- The backend private network that the Bare Metal Pool already provisions for your project (used for the RING internal plane); optionally, a second network for client-facing S3/NFS traffic, which Terraform can create for you.

:::info
This guide assumes familiarity with core RING concepts (rings, connectors, COS/ARC data protection, the supervisor role). For background on any of these, see the [Scality documentation portal](https://documentation.scality.com).
:::

## Instructions

### 1. Authenticating to OPCP

Export your application credential so both the OpenStack CLI and Terraform can use it:

```bash
export OS_AUTH_TYPE=v3applicationcredential
export OS_AUTH_URL=<opcp-keystone-endpoint>
export OS_IDENTITY_API_VERSION=3
export OS_REGION_NAME=<opcp-region>
export OS_INTERFACE=public
export OS_APPLICATION_CREDENTIAL_ID="<application-credential-id>"
export OS_APPLICATION_CREDENTIAL_SECRET="<application-credential-secret>"
```

Verify it works:

```bash
openstack token issue -f value -c id
```

:::tip
In some OPCP regions, the service catalog advertises endpoints that are not reachable from your network. If `openstack` commands fail to connect after a successful `openstack token issue`, add explicit per-service endpoint overrides (`OS_COMPUTE_ENDPOINT_OVERRIDE`, `OS_IMAGE_ENDPOINT_OVERRIDE`, `OS_NETWORK_ENDPOINT_OVERRIDE`, `OS_BAREMETAL_ENDPOINT_OVERRIDE`, `OS_VOLUME_ENDPOINT_OVERRIDE`) pointing at the addresses your project was given. Terraform's OpenStack provider accepts the same overrides in its provider block.
:::

### 2. Uploading the RING images to Glance

Upload the supervisor and storage images provided by Scality, giving each a distinct name:

```bash
openstack image create --disk-format qcow2 --container-format bare \
  --file <path-to-supervisor-image>.qcow2 "<supervisor-image-name>"

openstack image create --disk-format qcow2 --container-format bare \
  --file <path-to-storage-image>.qcow2 "<storage-image-name>"
```

:::warning
Image names must be unique in the project. Uploading under a name that already exists in Glance will make your Terraform plan fail rather than silently reuse the old image, so pick a new name whenever you upload a rebuilt image (e.g. append a version suffix).
:::

Building or customising these images is outside the scope of this guide — see the note under [Requirements](#requirements) for how to obtain them.

### 3. Preparing the bare-metal nodes

Two node-level settings live outside Terraform, because the standard OpenStack Ironic driver has no Terraform resource for them. Configure these settings **before** the nodes are deployed — they can only be changed on nodes that carry no instance, and redoing them later requires tearing the instance down first.

**Network bonding (LACP).** RING expects each node to present its network(s) as a bonded interface (`bond0` for the backend plane and, if you use a frontend network, `bond1`), so that losing a single NIC or top-of-rack switch does not take the node down. Configure an 802.3ad (LACP) port group per plane directly in OpenStack Ironic, following OVHcloud's [How to setup LACP on a Node](https://docs.ovhcloud.com/it/guides/hosted-private-cloud/opcp/how-to-setup-lacp-on-node.md) guide. Use the `layer3+4` transmit-hash policy so traffic spreads across both members of the bond (the kernel default, `layer2`, pins every flow to a single link).

:::warning
LACP port groups can only be created on nodes with no active instance. If a node already has an instance, tear it down first — the OVHcloud guide does not cover reconfiguring bonding on a node in production.
:::

**Software RAID for the OS disk (optional).** If you want the OS disk mirrored rather than relying on a hardware RAID controller, configure a RAID-1 `target_raid_config` on each node following OVHcloud's [How to configure a software RAID on a node](https://docs.ovhcloud.com/it/guides/hosted-private-cloud/opcp/how-to-setup-softraid-on-node.md) guide, before the node's next deploy.

:::warning
Creating the RAID array erases the disks it is built from. Only enable it on nodes that carry no instance and no data you need, and make sure your storage/supervisor image is mdadm-capable (the root filesystem itself does not need to be pre-built on an md device, but the image must be able to assemble one at boot — mdadm installed, the mdraid module in the initramfs).
:::

### 4. Defining your Terraform configuration

RING does not ship a public Terraform module for OPCP, so this configuration is built directly from the OpenStack, `tls` and `random` providers. It needs, in order: a fixed backend IP per node, your sizing-tool CSV completed with those addresses, a dedicated deployment SSH keypair, the bare-metal instances themselves, and enough `cloud-init` configuration for the supervisor to drive the install once every node is up.

**Providers and node addresses**

```hcl
terraform {
  required_providers {
    openstack = {
      source  = "terraform-provider-openstack/openstack"
      version = "~> 3.0"
    }
    tls    = { source = "hashicorp/tls", version = "~> 4.0" }
    random = { source = "hashicorp/random", version = "~> 3.0" }
  }
}

provider "openstack" {
  # Reads OS_AUTH_URL, OS_APPLICATION_CREDENTIAL_ID, etc. from step 1's environment.
}

variable "backend_network_name" { type = string }
variable "backend_subnet_name"  { type = string }
variable "store_image_name"      { type = string }
variable "supervisor_image_name" { type = string }

variable "lacp_enabled" {
  type    = bool
  default = true
}

# The supervisor's private backend IP address (see step 9).
variable "supervisor_published_address" { type = string }

# One fixed backend IP per node, keyed by minion_id — must match cluster.csv exactly.
# RING's minimum topology on OPCP is one supervisor and three storage nodes.
variable "node_ips" {
  type = map(string)
  default = {
    "<supervisor-minion-id>" = "<supervisor-backend-ip>"
    "<storage-minion-id-1>"  = "<storage-1-backend-ip>"
    "<storage-minion-id-2>"  = "<storage-2-backend-ip>"
    "<storage-minion-id-3>"  = "<storage-3-backend-ip>"
  }
}
```

**Complete the sizing-tool CSV**

Your sizing-tool export may bundle more than one section (for example, ring-level parameters followed by a servers table). Extract only the servers table — one row per node, with its header — into its own file (for example, `servers.csv`), keeping `minion_id`, `role` and `enclosure` as provided and the address columns (`data_ip`, `data_iface`, `s3_ip`, `s3_iface`) blank. Terraform then fills in exactly those 4 columns:

```hcl
locals {
  csv_rows = csvdecode(file("${path.module}/servers.csv"))
  iface    = var.lacp_enabled ? "bond0" : "eth0"

  completed_rows = [
    for row in local.csv_rows : merge(row, {
      data_ip    = var.node_ips[row.minion_id]
      data_iface = local.iface
      s3_ip      = row.role == "supervisor" ? "" : var.node_ips[row.minion_id]
      s3_iface   = local.iface
    })
  ]

  completed_csv = csvencode(local.completed_rows)
  store_ips     = [for r in local.completed_rows : r.data_ip if r.role != "supervisor"]
  nodes         = { for r in local.completed_rows : r.minion_id => r }
}
```

**Network ports**

```hcl
data "openstack_networking_network_v2" "backend" {
  name = var.backend_network_name
}

data "openstack_networking_subnet_v2" "backend" {
  name = var.backend_subnet_name
}

resource "openstack_networking_port_v2" "backend" {
  for_each   = var.node_ips
  name       = "${each.key}-backend"
  network_id = data.openstack_networking_network_v2.backend.id

  fixed_ip {
    subnet_id  = data.openstack_networking_subnet_v2.backend.id
    ip_address = each.value
  }
}
```

**Deployment keypair and GUI password**

```hcl
resource "tls_private_key" "deploy" {
  algorithm = "RSA"
  rsa_bits  = 4096
}

resource "random_password" "gui" {
  length  = 20
  special = false
}
```

**Bare-metal instances and `cloud-init`**

```hcl
resource "openstack_compute_instance_v2" "node" {
  for_each = local.nodes

  name         = each.key
  image_name   = each.value.role == "supervisor" ? var.supervisor_image_name : var.store_image_name
  flavor_name  = each.value.enclosure
  config_drive = true

  network {
    port = openstack_networking_port_v2.backend[each.key].id
  }

  user_data = each.value.role == "supervisor" ? templatefile("${path.module}/cloudinit-supervisor.yaml.tftpl", {
    deploy_private_key = tls_private_key.deploy.private_key_openssh
    deploy_public_key  = tls_private_key.deploy.public_key_openssh
    store_ips          = local.store_ips
    cluster_csv        = local.completed_csv
    gui_password       = random_password.gui.result
    published_address  = var.supervisor_published_address
  }) : templatefile("${path.module}/cloudinit-store.yaml.tftpl", {
    deploy_public_key = tls_private_key.deploy.public_key_openssh
  })
}

output "port_ids" {
  value = { for k, p in openstack_networking_port_v2.backend : k => p.id }
}

output "gui_password" {
  value     = random_password.gui.result
  sensitive = true
}
```

`cloudinit-store.yaml.tftpl` only needs to authorise the deployment key as root, so the supervisor can reach the node:

```yaml
#cloud-config
users:
  - name: root
    ssh_authorized_keys:
      - ${deploy_public_key}
```

`cloudinit-supervisor.yaml.tftpl` authorises the same key, stages the completed CSV and the private half of the deployment key, then waits for every storage node to answer over SSH before launching the RING installer non-interactively and detached (so it survives the end of `cloud-init`):

```yaml
#cloud-config
users:
  - name: root
    ssh_authorized_keys:
      - ${deploy_public_key}

write_files:
  - path: /root/.ssh/id_rsa
    permissions: '0600'
    content: |
      ${indent(6, deploy_private_key)}
  - path: /var/tmp/scality/cluster.csv
    content: |
      ${indent(6, cluster_csv)}

runcmd:
  - |
    for ip in ${join(" ", store_ips)}; do
      timeout 3600 bash -c "until ssh -o StrictHostKeyChecking=no -i /root/.ssh/id_rsa root@$ip true; do sleep 10; done"
    done
    RING_GUI_PASSWORD=${gui_password} RING_SUPERVISOR_FQDN=${published_address} \
      setsid /root/<ring-installer-bundle>.run --yes --conf /var/tmp/scality/conf \
      > /var/tmp/scality/install.log 2>&1 &
```

:::tip
If your deployment also serves S3/NFS clients on a separate network from the RING backend plane, repeat the network-ports block for a second, frontend network (with no gateway and no DNS, so nodes keep their default route on the backend plane), attach a second `network { port = ... }` block to each `openstack_compute_instance_v2.node`, and point the CSV's `s3_ip` / `s3_iface` at that plane's address and bond (`bond1`) for the storage nodes instead of the backend one.
:::

### 5. Running a preflight check

Before running `terraform plan`, run the following read-only checks:

- The application credential and Terraform provider can both reach the project.
- Both the supervisor and storage images exist in Glance under the names your configuration references.
- At least as many nodes are in the `available` Ironic state as your `cluster.csv` requires.
- If `lacp_enabled` is set, every node you expect Terraform to pick already carries its LACP port groups (step 3) — Nova can select any `available` node, and the installer CSV bakes in bond names at plan time, so a node without bonding will boot with a mismatched network configuration.
- If you configured software RAID, its state matches what you intend for this deploy.

Fixing a gap now is far cheaper than fixing it after a bare-metal instance is provisioned.

### 6. Reviewing and applying the Terraform plan

```bash
terraform init
terraform plan -out=tfplan
terraform show tfplan > tfplan.txt
```

Read `tfplan.txt` carefully before applying:

:::warning
On bare metal, a Terraform _replace_ of an instance is a full Ironic redeploy of the physical machine — the node is wiped and reprovisioned from scratch. Look specifically for `must be replaced` or `will be destroyed` on any bare-metal instance, network port, or the deployment keypair, and, if you see one you did not expect, find out why before applying.
:::

```bash
terraform apply tfplan
```

### 7. Letting the automated install run

Because the nodes cannot be reached over SSH before they boot, the RING installer is launched by `cloud-init` itself rather than by an operator running a script:

- Terraform generates a dedicated deployment keypair. The public half is added to every node's root `authorized_keys`; the private half rides only the supervisor's `cloud-init`, so the supervisor — and only the supervisor — can reach every storage node as root.
- At first boot, the supervisor writes the completed install CSV and installer configuration to local disk, then polls root SSH to every storage node (boot order across bare-metal nodes is not guaranteed, so this typically runs with a generous timeout, of the order of an hour).
- Once every node answers, the supervisor launches the RING installer non-interactively and detached, so it survives the end of `cloud-init` and is not affected by a dropped connection to the supervisor.

There is no operator action in this step — it is the point of the `cloud-init`-driven design.

### 8. Verifying the deployment

Before assuming the install is progressing normally, confirm the network layer matches what the CSV assumed at plan time:

```bash
openstack baremetal port group list --node <node> --long -f json | jq '.[] | {name, internal_info}'
```

Each backend port group should carry the node's port ID from your Terraform `port_ids` output as `internal_info.tenant_vif_port_id` (and, if you configured a frontend network, each frontend port group should carry the matching `frontend_port_ids` entry). This mapping is not contractually guaranteed by Ironic, so it is worth checking after every deploy, not only the first.

From the supervisor, confirm in the guest OS that each bond came up with the expected address and that the default route points at the backend gateway, then follow the install log:

```bash
tail -f /var/tmp/scality/install.log
```

:::info
Each time it is launched, the installer exits immediately if it finds a `done` marker or a still-running process, so rebooting a node mid-install will not restart a finished or in-progress install.
:::

### 9. Accessing the RING supervisor UI

Retrieve the GUI/admin password generated by Terraform:

```bash
terraform output -raw gui_password
```

Then, from a host with network access to the backend plane, open `https://<supervisor_published_address>/gui/` in a browser, where `<supervisor_published_address>` is the supervisor's private backend IP address. Accept the self-signed certificate on first access and log in as an administrator.

:::info
If you have not yet activated a RING licence, do so now through the supervisor UI or as guided by [Scality support](https://support.scality.com). RING enforces licence limits once the evaluation period ends.
:::

## Conclusion

At this point, Terraform owns the network ports, the bare-metal instances and the generated credentials, and `cloud-init` has driven an unattended RING install across every node. You have a running Scality RING cluster on OPCP, ready for licence activation (if not already done) and for configuring S3/IAM and, if applicable, file connectors. For everything past this point — sizing, ring topology changes, connector configuration, upgrades — consult the [Scality documentation portal](https://documentation.scality.com), and reach out to [Scality support](https://support.scality.com) for anything licence- or issue-related.

## Go further

- [Scality documentation portal](https://documentation.scality.com) — RING concepts, configuration and operations beyond initial deployment
- [Scality support](https://support.scality.com) — licensing and technical support
- [OPCP overview](https://docs.ovhcloud.com/it/guides/hosted-private-cloud/opcp/landing-page-opcp.md)
- [How to use Terraform](https://docs.ovhcloud.com/it/guides/hosted-private-cloud/opcp/use-terraform.md)
- [How to setup LACP on a Node](https://docs.ovhcloud.com/it/guides/hosted-private-cloud/opcp/how-to-setup-lacp-on-node.md)
- [How to configure a software RAID on a node](https://docs.ovhcloud.com/it/guides/hosted-private-cloud/opcp/how-to-setup-softraid-on-node.md)
- [Lifecycle of an OPCP node](https://docs.ovhcloud.com/it/guides/hosted-private-cloud/opcp/node-lifecycle.md)
- [OPCP Core compatibility matrix](https://docs.ovhcloud.com/it/guides/hosted-private-cloud/opcp/opcp-compatibility-matrix.md)
- Background on Ironic/Nova image requirements: [custom CentOS Stream image](https://docs.ovhcloud.com/it/guides/hosted-private-cloud/opcp/how-to-create-centos-image.md)

For training or technical assistance implementing our solutions, contact your sales representative or visit our [Professional Services](https://www.ovhcloud.com/it/professional-services/) page to request a quote and have your project analysed by our experts.

Join our [community of users](https://community.ovhcloud.com/).

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.