Orchestrating Physical Infrastructure: A Deep Dive into Terraform for Bare Metal Deployment

The evolution of cloud infrastructure has consistently pushed the boundaries of what infrastructure as code (IaC) can achieve. While virtualization has long been the domain of Terraform’s dominance, the integration of bare metal hardware into code-driven workflows represents a significant paradigm shift for DevOps engineers and systems architects. Manually provisioning servers is inherently slow and prone to human error, creating inconsistent environments that complicate scaling and disaster recovery. Whether addressing legacy server sprawl or establishing a new edge computing node, the ability to define physical servers in declarative code and provision them with a single command is no longer a novelty but a operational necessity. This guide provides a comprehensive technical analysis of how to provision bare metal servers using Terraform, examining the architectural underpinnings, specific provider implementations, and the operational mechanics of state management, resource dependency, and multi-instance deployment across major providers including phoenixNAP, Cherry Servers, and IBM Cloud.

The Architecture of Terraform Providers

Before writing any configuration code, it is critical to understand the mechanical interaction between Terraform and external platforms. Terraform utilizes providers to interact with external systems. A provider is effectively a plugin that translates declarative configuration into specific Application Programming Interface (API) calls. In the context of bare metal, this translation is particularly complex because the underlying hardware lacks the ephemeral nature of virtual machines. The provider must communicate with the hardware management API of the cloud provider to instruct physical components to power on, allocate network resources, and install operating system images.

The workflow begins with the initialization of the state file and the configuration of the provider block. Each provider requires specific authentication credentials and endpoint information. For example, the phoenixNAP provider requires a clientId and clientSecret, which are obtained from the user’s account dashboard. These credentials are typically stored in a config.yaml file or passed via environment variables to avoid hardcoding secrets into the version-controlled Terraform files. The provider version must also be pinned to ensure reproducibility; for instance, the phoenixNAP provider is often locked to version 0.6.0 to prevent breaking changes from newer releases.

The following table outlines the basic authentication and provider configuration requirements for two distinct bare metal providers.

Provider Authentication Mechanism Key Configuration Files Provider Source String
phoenixNAP Client ID and Client Secret config.yaml phoenixnap/pnap
Cherry Servers Project ID and API Token terraform.tfvars / Env Vars cherryservers/cherryservers

The source code for these providers and their accompanying documentation is available on the official Terraform registry pages. By strictly adhering to the provider version constraints, engineers ensure that the API calls generated during the plan phase match the actual capabilities of the backend infrastructure. This is crucial in bare metal environments where hardware specs (such as CPU model and RAM capacity) are fixed at the time of order and do not dynamically adjust like virtual instances.

Defining Resources: phoenixNAP Implementation

The phoenixNAP Bare Metal Cloud (BMC) provider offers a streamlined approach to deploying physical servers. To initiate a deployment, the user must first install Terraform locally or on a remote server and gather the necessary authentication data. Once the environment is set up, the configuration file declares the use of the pnap provider.

A basic deployment involves defining a pnap_server resource. This resource block specifies the desired state of the infrastructure, including the hostname, operating system image, server type, and location. The following code block demonstrates the creation of a basic Bare Metal Cloud server configuration using the s1.c1.small type with an Ubuntu OS in the Phoenix data center.

```hcl
terraform {
required_providers {
pnap = {
source = "phoenixnap/pnap"
version = "0.6.0"
}
}
}

provider "pnap" {
# Configuration options containing clientId and clientSecret
}

resource "pnapserver" "My-First-BMC-Server" {
hostname = "your-hostname"
os = "ubuntu/bionic"
type = "s1.c1.small"
location = "PHX"
ssh
keys = ["ssh-rsa..."]
# action = "powered-on"
}
```

In this configuration, the type argument dictates the hardware specification, while the location argument determines the physical data center. The ssh_keys attribute accepts a list of SSH public keys, which the provider injects into the server’s authorized keys file upon provisioning. A critical operational parameter is the action argument, which denotes the power state of the server after provisioning. Valid values include reboot, reset, power-on, power-off, and shutdown. While power-on is often the desired default for immediate availability, some workflows may require the server to remain powered off until a specific dependency is met. The action argument provides fine-grained control over the lifecycle, allowing engineers to automate complex boot sequences or maintenance windows.

Deep Dive into Cherry Servers Provisioning

The Cherry Servers provider illustrates the broader workflow applicable to most Terraform-supported bare metal providers, including Equinix Metal and Hetzner. This workflow emphasizes the separation of concerns between identity management (SSH keys) and compute resources (servers). Prerequisites for this workflow include a Cherry Servers account, Terraform v1.5.0 or later, and a basic familiarity with the command line.

The configuration process involves defining two primary resources: an SSH key and a server. The SSH key resource uploads the local public key to the provider’s platform. When the server boots, the provider injects this key into the operating system, enabling root access. The server resource then references the SSH key using a resource reference, such as cherryservers_ssh_key.my_key.id. This reference serves a dual purpose: it passes the unique identifier of the key to the server resource, and it establishes a dependency graph. Terraform uses this dependency to determine the correct order of operations, ensuring the SSH key is created before the server begins provisioning.

The following table details the common bare metal plans available through Cherry Servers and their corresponding hardware attributes.

Plan Name Description Typical Use Case
e5-1620v4 Single socket Intel Xeon E5-1620 v4 Development, Small Scale Production
2x-e5-2620v4 Dual socket Intel Xeon E5-2620 v4 Medium Scale Production, Database Nodes
2x-e5-2650v4 Dual socket Intel Xeon E5-2650 v4 High Performance Compute, Large Databases

The main configuration file (main.tf) is defined as follows:

```hcl

Upload your SSH public key to Cherry Servers.

This lets you SSH into the server once it is provisioned.

resource "cherryserverssshkey" "mykey" {
name = "terraform-key"
public
key = file(var.sshpublickey_path)
}

Provision the bare metal server.

resource "cherryserversserver" "baremetal" {
projectid = var.projectid
region = var.region
plan = var.plan
image = var.image
hostname = var.hostname
# Associate the SSH key so you can log in after provisioning.
sshkeyids = [cherryserverssshkey.my_key.id]
tags = {
"Environment" = "development"
"ManagedBy" = "terraform"
}
}
```

The tags block is optional but highly recommended for operational clarity. Tags such as Environment and ManagedBy allow for automated cost allocation, compliance tracking, and filtering within the provider’s dashboard. The image attribute specifies the operating system, such as ubuntu_24_04_64bit. The plan attribute determines the physical hardware, linking the logical resource to the physical constraints of the data center.

State Management and Execution Planning

Once the resources are defined, the terraform plan command initiates the execution plan. Terraform calculates the delta between the current state and the desired state. For new deployments, the plan indicates resource creation using the + symbol. The plan output provides a preview of the changes, including attribute values that are known only after the apply phase, marked as (known after apply).

For example, the plan output for the Cherry Servers deployment indicates that id, ip_addresses, and power_state are not known until the API responds with the actual server details. The plan confirms the dependency resolution, showing that both the cherryservers_server.bare_metal and cherryservers_ssh_key.my_key resources will be created. The output also includes the planned changes to any output variables defined in the configuration.

A critical aspect of team-based workflows is state management. If working in a team, the state file must be stored in a remote backend, such as Terraform Cloud or an S3 bucket. This ensures that all team members use the same state, preventing conflicts and divergent views of the infrastructure. Local state files are suitable for single-user development but pose significant risks in collaborative environments where concurrent modifications can lead to state corruption or unintended resource destruction.

Multi-Instance Deployment and Count Parameters

Scalability is a core requirement of modern infrastructure. Terraform facilitates the deployment of multiple identical servers through the count meta-argument. This argument allows the creation of one instance of a resource for each count value, enabling linear scaling of infrastructure without duplicating code blocks.

To deploy a cluster of three servers, the cherryservers_server resource block is updated to include count = 3. The hostname attribute is dynamically generated using count.index + 1, ensuring each server has a unique, predictable name (e.g., bare-metal-01, bare-metal-02, bare-metal-03). The tags block can also be updated to include the index, providing a clear identifier for each node in monitoring and logging systems.

hcl resource "cherryservers_server" "bare_metal" { count = 3 project_id = var.project_id region = var.region plan = var.plan image = var.image hostname = "bare-metal-0${count.index + 1}" ssh_key_ids = [cherryservers_ssh_key.my_key.id] tags = { "Environment" = "development" "Index" = count.index } }

The outputs.tf file is updated to aggregate the IP addresses of all servers. This is achieved using a for expression that iterates over the list of servers and extracts the primary IP address from the ip_addresses attribute.

hcl output "server_ips" { description = "Primary IP addresses of all servers" value = [for server in cherryservers_server.bare_metal : [for ip in server.ip_addresses : ip.address if ip.type == "primary-ip"][0]] }

When terraform plan is executed in this multi-instance scenario, the plan reflects 4 to add, 0 to change, 0 to destroy: three servers and one SSH key. This atomicity ensures that if any part of the plan fails, the entire operation is halted, maintaining infrastructure consistency.

Advanced Subnet Selection in IBM Cloud

For complex enterprise environments, such as IBM Cloud, the deployment of bare metal servers requires sophisticated network management. The IBM Cloud Bare Metal Server Deployment Module provisions servers in a flexible and scalable manner, supporting single or multiple server deployments. This module dynamically distributes servers across one or more subnets, ensuring that each instance is uniquely named and correctly attached to the Virtual Private Cloud (VPC) network.

The module addresses common Terraform plan-time issues by gracefully handling complex dependencies. It ensures high availability with intelligent subnet selection, a critical feature for workloads that require redundancy across multiple network segments. The following configuration snippet demonstrates the provider requirements for the IBM module.

hcl terraform { required_version = ">= 1.9.0" required_providers { ibm = { source = "IBM-Cloud/ibm" version = "X.Y.Z" } } }

The required_version constraint ensures that the Terraform client is recent enough to support the features used by the module. The required_providers block locks the IBM provider version to satisfy the module’s internal constraints. This version locking is essential in enterprise environments where provider updates can introduce breaking changes to resource attributes or API behaviors.

Post-Provisioning Connectivity

After the terraform apply command completes, the servers are in an active state. The output of the apply command provides the necessary connection details. For the Cherry Servers example, the output includes the server_id, server_ip, and a formatted ssh_connection string. The server_ip is the primary public IP address assigned to the server, while the server_id is the unique identifier used for API management.

To verify connectivity, engineers use the SSH key uploaded during the provisioning phase. The SSH command connects to the server using the root user, as the provider injects the public key into the root user’s authorized keys file.

bash ssh [email protected]

This immediate access is a significant advantage of code-driven provisioning, eliminating the need for manual password configuration or console logins. The power_state attribute in the Terraform state confirms that the server is on, and the state attribute confirms it is active. The pricing attribute provides visibility into the cost structure, which for bare metal is typically a fixed hourly or monthly rate, unlike the variable pricing of virtual instances.

Conclusion

The integration of Terraform with bare metal infrastructure providers like phoenixNAP, Cherry Servers, and IBM Cloud transforms physical server management from a manual, error-prone task into a repeatable, automated process. By leveraging providers, engineers can translate declarative code into API calls that provision hardware, install operating systems, and configure network access. The workflow is characterized by strict dependency management, where resources like SSH keys are created before the servers that require them. Multi-instance deployment via the count argument and advanced subnet selection modules enable the scaling of bare metal infrastructure to meet enterprise demands.

The reliability of this system depends on meticulous state management and version control. Storing state in remote backends and pinning provider versions are non-negotiable practices for team-based operations. The ability to preview changes via terraform plan and apply them atomically ensures that infrastructure changes are safe and predictable. As hardware diversity increases and edge computing becomes more prevalent, the ability to code-manage bare metal servers will only become more critical. This approach not only speeds up provisioning but also enforces consistency, reduces operational risk, and provides an auditable trail of all infrastructure changes.

Sources

  1. phoenixNAP Blog: Terraform Infrastructure as Code
  2. Cherry Servers Blog: Provision Bare Metal Servers with Terraform
  3. GitHub: terraform-ibm-modules/terraform-ibm-bare-metal-vpc

Related Posts