IBM Power Systems Virtual Server (PowerVS) is a great fit for data-intensive workloads. In this post, we’ll walk through deploying a single-node Apache Cassandra 5.0 instance on PowerVS using a RHEL 10 Bring-Your-Own-License (BYOL) image, fully automated with Terraform and a cloud-init shell script.
By the end you’ll have:
- A running PowerVS LPAR (s1022) on RHEL 10
- IBM Semeru Runtime (OpenJ9) for Java on ppc64le
- Apache Cassandra 5.0 installed and ready to start
- A public network interface for SSH access
Architecture Overview
The deployment is split into two layers:
Infrastructure (Terraform)
| Resource | Purpose |
|---|---|
ibm_pi_network | Creates a public pub-vlan network for external access |
ibm_pi_instance | Provisions the LPAR using the RHEL 10 BYOL stock image |
null_resource | SSHes into the instance after boot and runs the init script |
Configuration (init.sh)
Once the LPAR is up, Terraform copies a templated init.sh script to the instance and executes it over SSH. The script:
- Registers the OS with Red Hat Subscription Manager (RHSM)
- Enables the RHEL 10 ppc64le base and appstream repos
- Updates the OS and installs tooling (
wget,curl,jq, etc.) - Downloads and installs IBM Semeru Java 17 LTS for
ppc64lefrom GitHub Releases - Registers the
cassandraYUM repo and installs Apache Cassandra 5.0 - Drops a
cassandra-manager.servicesystemd unit (installed but not auto-started — you control when it runs)
Prerequisites
Before you begin, make sure you have:
- An IBM Cloud account with PowerVS permissions
- A pre-provisioned PowerVS workspace (get the workspace GUID from the UI)
- A PowerVS SSH key registered in the workspace
- Red Hat credentials (RHSM username and password) for a RHEL subscription
- Terraform ≥ 1.3 installed locally
- The IBM Cloud CLI with the PowerVS plugin (
ibmcloud plugin install power-iaas)
RHEL 10 BYOL image: This example uses the stock catalog image ID
585ca713-f303-45f0-a836-e9dd4f4c8f3b(RHEL10-BYOL) which is already available in PowerVS — no COS bucket or manual image upload required.
Repository Layout
.
├── main.tf # Provider, network, instance, and provisioner
├── variables.tf # Input variable declarations
├── outputs.tf # Instance ID and IP outputs
├── init.sh # cloud-init / SSH-executed bootstrap script
└── terraform.tfvars.example # Template — copy to terraform.tfvars
Step 1 — Configure Your Variables
Copy the example variables file and fill in your values:
cp terraform.tfvars.example terraform.tfvars
Then edit terraform.tfvars:
ibmcloud_api_key = "YOUR_IBM_CLOUD_API_KEY"
workspace_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
region = "us-south"
zone = "dal12"
ssh_key_name = "my-powervs-ssh-key"
ssh_private_key_path = "~/.ssh/id_rsa"
memory = 16
processors = 2
rhsm_username = "your-redhat-username"
rhsm_password = "your-redhat-password"
Never commit
terraform.tfvarsto source control — it contains your IBM Cloud API key and RHSM credentials. The.gitignorein this repo already excludes it, and all three sensitive variables are declared withsensitive = trueinvariables.tfso Terraform redacts them from plan/apply output.
Variable Reference
| Variable | Description | Default |
|---|---|---|
ibmcloud_api_key | IBM Cloud API key | — |
workspace_id | PowerVS workspace GUID | — |
region | IBM Cloud region (e.g. us-south) | — |
zone | IBM Cloud zone (e.g. dal12) | — |
ssh_key_name | SSH key registered in the PowerVS workspace | — |
ssh_private_key_path | Local path to the matching private key | ~/.ssh/id_rsa |
memory | Instance memory in GiB | 16 |
processors | Number of virtual processors | 2 |
rhsm_username | Red Hat Subscription Manager username | — |
rhsm_password | Red Hat Subscription Manager password | — |
Step 2 — Understand the Terraform Configuration
Provider
main.tf pins the IBM Cloud provider at ~> 2.4.0 and the HashiCorp null provider for the remote-exec provisioner:
terraform {
required_providers {
ibm = {
source = "IBM-Cloud/ibm"
version = "~> 2.4.0"
}
null = {
source = "hashicorp/null"
version = "~> 3.0"
}
}
}
provider "ibm" {
ibmcloud_api_key = var.ibmcloud_api_key
region = var.region
zone = var.zone
}
Public Network
A pub-vlan network is created to give the instance an external IP for SSH access:
resource "ibm_pi_network" "public_net" {
pi_cloud_instance_id = var.workspace_id
pi_network_name = "cassandra-public-net"
pi_network_type = "pub-vlan"
}
PowerVS Instance
The LPAR is an s1022 system (Power10) using a shared processor, the stock RHEL 10 BYOL image, and attached to the public network:
resource "ibm_pi_instance" "cassandra_node" {
pi_cloud_instance_id = var.workspace_id
pi_memory = var.memory
pi_processors = var.processors
pi_instance_name = "cassandra-rhel10"
pi_proc_type = "shared"
pi_sys_type = "s1022"
pi_image_id = "585ca713-f303-45f0-a836-e9dd4f4c8f3b"
pi_key_pair_name = var.ssh_key_name
pi_health_status = "WARNING"
pi_network {
network_id = ibm_pi_network.public_net.network_id
}
depends_on = [ibm_pi_network.public_net]
}
pi_health_status = "WARNING"lets Terraform proceed even while the instance is still booting, which is normal — thenull_resourceprovisioner handles the wait via SSH.
Remote Provisioner
After the instance is up, Terraform templates init.sh with your RHSM credentials and copies it over SSH, then executes it:
resource "null_resource" "cassandra_init" {
triggers = {
instance_id = ibm_pi_instance.cassandra_node.instance_id
script_hash = filemd5("${path.module}/init.sh")
}
connection {
type = "ssh"
user = "root"
host = ibm_pi_instance.cassandra_node.pi_network[0].external_ip
agent = true
timeout = "10m"
}
provisioner "file" {
content = templatefile("${path.module}/init.sh", {
rhsm_username = var.rhsm_username
rhsm_password = var.rhsm_password
})
destination = "/root/init.sh"
}
provisioner "remote-exec" {
inline = [
"chmod +x /root/init.sh",
"bash /root/init.sh",
]
}
}
The script_hash trigger means Terraform will re-run the provisioner if you change init.sh, which is handy during development.
Step 3 — The Bootstrap Script (init.sh)
init.sh is a Bash script executed on the instance as root. Here’s what it does, step by step.
RHSM Registration
subscription-manager register \
--username="${rhsm_username}" \
--password="${rhsm_password}" \
--force
subscription-manager repos \
--enable=rhel-10-for-ppc64le-baseos-rpms \
--enable=rhel-10-for-ppc64le-appstream-rpms
The --force flag handles re-registration gracefully if the instance was previously registered. The correct repo slugs for RHEL 10 on Power LE (ppc64le) are rhel-10-for-ppc64le-*.
OS Update and Tooling
dnf update -y
dnf install -y wget tar gzip jq curl
IBM Semeru Java 17 (ppc64le)
Cassandra requires Java, and IBM Semeru Runtime (OpenJ9) is the recommended JVM for Power. The script fetches the latest release URL dynamically from the GitHub API:
SEMERU_LATEST_URL=$(curl -s https://api.github.com/repos/ibmruntimes/semeru17-binaries/releases/latest \
| jq -r '.assets[] | select(.name | contains("jdk_ppc64le_linux")) | select(.name | endswith(".tar.gz")) | .browser_download_url')
wget -qO /tmp/semeru.tar.gz "$SEMERU_LATEST_URL"
mkdir -p /opt/ibm/semeru
tar -xzf /tmp/semeru.tar.gz -C /opt/ibm/semeru --strip-components=1
rm -f /tmp/semeru.tar.gz
alternatives --install /usr/bin/java java /opt/ibm/semeru/bin/java 1
echo "export JAVA_HOME=/opt/ibm/semeru" > /etc/profile.d/semeru.sh
export JAVA_HOME=/opt/ibm/semeru
export PATH="$JAVA_HOME/bin:$PATH"
Installing under /opt/ibm/semeru keeps it clean and separate from the system Java. The alternatives registration makes java available system-wide.
Apache Cassandra 5.0
The official Apache Cassandra RPM repo is added and the package installed. --skip-broken is used because Cassandra’s bundled JVM dependency is skipped in favour of Semeru:
rpm --import https://downloads.apache.org/cassandra/KEYS
cat <<'EOF' > /etc/yum.repos.d/cassandra.repo
[cassandra]
name=Apache Cassandra
baseurl=https://redhat.cassandra.apache.org/50x/
gpgcheck=0
repo_gpgcheck=0
gpgkey=https://downloads.apache.org/cassandra/KEYS
EOF
dnf clean all && dnf makecache dnf install -y cassandra --skip-broken
Systemd Service Unit
Rather than auto-starting Cassandra, the script drops a custom cassandra-manager.service unit that explicitly sets JAVA_HOME to the Semeru installation. This prevents Cassandra from picking up an incorrect JVM:
cat <<'EOF' > /etc/systemd/system/cassandra-manager.service
[Unit]
Description=Apache Cassandra Lifecycle Manager
After=network-online.target
Wants=network-online.target
[Service]
Type=forking
User=cassandra
Group=cassandra
Environment="JAVA_HOME=/opt/ibm/semeru"
Environment="PATH=/opt/ibm/semeru/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin"
ExecStart=/usr/sbin/cassandra -p /var/run/cassandra/cassandra.pid
PIDFile=/var/run/cassandra/cassandra.pid
RuntimeDirectory=cassandra
TimeoutStartSec=120
Restart=on-failure
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
The service is not enabled or auto-started — you decide when Cassandra runs.
Step 4 — Deploy
Initialize
terraform init
This downloads the IBM Cloud and null providers.
Plan
terraform plan -var-file="terraform.tfvars"
Review the output — you should see three resources being created: ibm_pi_network.public_net, ibm_pi_instance.cassandra_node, and null_resource.cassandra_init.
Apply
terraform apply -var-file="terraform.tfvars"
Total deployment time is approximately 10–15 minutes:
- Network creation: ~1 minute
- Instance boot: ~5 minutes
init.shexecution (OS update + Java + Cassandra): ~5–10 minutes
Step 5 — Verify
Once terraform apply completes, grab the IP from the outputs:
terraform output instance_external_ip_address
SSH in as root:
ssh root@<external_ip>
Confirm IBM Semeru is the active JVM:
java -version
# Expected output:
# openjdk version "17.x.x" ...
# IBM Semeru Runtime Open Edition ...
# Eclipse OpenJ9 ...
Start Cassandra:
systemctl start cassandra-manager
systemctl status cassandra-manager
Once Cassandra is up (give it 30–60 seconds), verify the node is healthy:
nodetool status
You should see a single node listed as UN (Up, Normal):
Datacenter: datacenter1
=======================
Status=Up/Down
|/ State=Normal/Leaving/Joining/Moving
-- Address Load Tokens Owns Host ID Rack
UN 127.0.0.1 ... 16 100.0% <uuid> rack1
To persist Cassandra across reboots:
systemctl enable cassandra-manager
Outputs Reference
| Output | Description |
|---|---|
instance_id | PowerVS LPAR instance ID |
instance_ip_address | Private IP on the attached network |
instance_external_ip_address | Public/external IP (used for SSH) |
Security Considerations
- Credentials in
terraform.tfvars— never commit this file. It is already listed in.gitignore. - Sensitive variables —
ibmcloud_api_key,rhsm_username, andrhsm_passwordare markedsensitive = trueinvariables.tf, so Terraform will not print them in plan or apply output. - SSH agent forwarding — the provisioner connection uses
agent = true, which relies on your local SSH agent. Make sure the private key matchingssh_key_nameis loaded (ssh-add ~/.ssh/id_rsa). - Firewall — the
pub-vlannetwork exposes the instance publicly. Consider restricting inbound access to port22(SSH) and Cassandra’s native transport port9042to trusted CIDRs via IBM Cloud security groups.
Cleaning Up
To destroy all resources created by this deployment:
terraform destroy -var-file="terraform.tfvars"
This will remove the PowerVS instance and the public network. The RHEL 10 stock image is not deleted (it is a shared catalog image, not a user-imported resource).
Summary
This walkthrough showed how to:
- Use Terraform with the
IBM-Cloud/ibmprovider to stand up a PowerVS LPAR on RHEL 10 - Bootstrap the instance via SSH using a templated shell script
- Install IBM Semeru Runtime (OpenJ9) for
ppc64le— the right JVM for Power - Add the official Apache Cassandra 5.0 RPM repo and install Cassandra
- Register a custom systemd service that pins
JAVA_HOMEto the Semeru installation
The full source is available in this repository — clone it, fill in your terraform.tfvars, and you’ll have Cassandra running on PowerVS in under 15 minutes.
Note the source is attached



