In this post I’ll walk through how I fully automated the provisioning of an IBM MQ server on my Proxmox homelab using Terraform and Ansible. With a single terraform apply, the entire stack builds itself — VM creation, OS configuration, IBM MQ installation, queue manager setup, and systemd service registration.
Architecture Overview
homelab02 (Terraform + Ansible host)
│
│ terraform apply
▼
Proxmox API (192.168.1.14:8006)
│
├── ① Clone Ubuntu 24.04 template
├── ② Apply cloud-init (kernel params, qemu-agent)
├── ③ Wait for VM ready
└── ④ Run Ansible → IBM MQ install + QM1 config
Tools used:
- Proxmox VE 9.2.2 — hypervisor
- Terraform with
bpg/proxmoxprovider — VM provisioning - cloud-init — first-boot configuration
- Ansible — IBM MQ installation and configuration
- IBM MQ 9.3.0 — message broker
Prerequisites
On the Proxmox server
- Proxmox VE 9.x.x installed
- An Ubuntu 24.04 cloud-init template (VM ID 9000)
- Snippets enabled on local storage
- An API token with Administrator permissions
On the Ansible/Terraform host (homelab02)
- Terraform installed
- Ansible installed
ansible.posixcollection installed- IBM MQ binary downloaded from IBM
- SSH key pair generated
Step 1 — Create Proxmox API Token
In the Proxmox web UI:
Datacenter → Permissions → API Tokens → Add
User: root@pam
Token ID: proxmox_api_token_id
Privilege Separation: unchecked
Then assign Administrator permissions:
Datacenter → Permissions → Add → API Token Permission
Path: /
Token: root@pam!proxmox_api_token_id
Role: Administrator
Test the token from your Ansible host:
curl -sk https://192.168.1.14:8006/api2/json/version \
-H 'Authorization: PVEAPIToken=root@pam!proxmox_api_token_id=<your-secret>'
You should get a JSON response like:
{"data":{"release":"9.2","version":"9.2.2","repoid":"b9984c6d90a4bd80"}}
Step 2 — Create Ubuntu 24.04 Cloud-Init Template
Run these commands on the Proxmox server:
# Download Ubuntu 24.04 cloud image
cd /tmp
wget https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img
# Create base VM
qm create 9000 --name ubuntu-2404-template --memory 2048 --cores 2 \
--net0 virtio,bridge=vmbr0
# Import the cloud image
qm importdisk 9000 /tmp/noble-server-cloudimg-amd64.img local-lvm
# Configure the VM
qm set 9000 --scsihw virtio-scsi-pci --scsi0 local-lvm:vm-9000-disk-0
qm set 9000 --ide2 local-lvm:cloudinit
qm set 9000 --boot c --bootdisk scsi0
qm set 9000 --serial0 socket --vga serial0
qm set 9000 --agent enabled=1
# Convert to template
qm template 9000
Step 3 — Enable Snippets on Local Storage
The Terraform provider uploads cloud-init files via SSH as snippets. Enable this in Proxmox:
pvesm set local --content backup,iso,vztmpl,snippets
Step 4 — Project Structure
proxmox-terraform/
├── main.tf
├── cloud-init.yaml
└── ibmmq-ansible/
├── playbook.yml
├── inventory.ini
├── group_vars/
│ └── all.yml
└── roles/
└── ibmmq/
├── tasks/
│ ├── main.yml
│ ├── 01_users_groups.yml
│ ├── 02_system_config.yml
│ ├── 03_install_mq.yml
│ ├── 04_environment.yml
│ ├── 05_configure_mq.yml
│ └── 06_service.yml
├── templates/
│ ├── mqsc_config.j2
│ └── ibmmq.service.j2
└── handlers/
└── main.yml
Step 5 — cloud-init.yaml
This runs on first boot and configures the VM before Ansible takes over. The key things it does are creating the user, setting kernel parameters required by IBM MQ, and installing the qemu guest agent.
#cloud-config
ssh_pwauth: true
users:
- name: indunil
sudo: ALL=(ALL) NOPASSWD:ALL
shell: /bin/bash
lock_passwd: false
groups: sudo
ssh_authorized_keys:
- ssh-ed25519 AAAA... your-public-key
chpasswd:
list: |
indunil:yourpassword
expire: false
package_update: true
package_upgrade: true
packages:
- qemu-guest-agent
- curl
- git
- python3
write_files:
- path: /etc/sysctl.d/99-ibmmq.conf
owner: root:root
permissions: '0644'
content: |
# IBM MQ Kernel Parameters
kernel.shmmax=268435456
kernel.shmall=65536
kernel.shmmni=4096
kernel.sem=500 256000 250 1024
runcmd:
- systemctl enable qemu-guest-agent
- systemctl start qemu-guest-agent
- sysctl -p /etc/sysctl.d/99-ibmmq.conf
Important: The kernel shared memory parameters are critical for IBM MQ. Without
kernel.shmmax=268435456, the queue manager creation fails withAMQ8101S error 893.
Step 6 — main.tf
The Terraform configuration handles VM creation, waits for cloud-init to complete, then hands off to Ansible.
terraform {
required_providers {
proxmox = {
source = "bpg/proxmox"
version = "~> 0.78"
}
}
}
provider "proxmox" {
endpoint = "https://192.168.1.14:8006"
api_token = "root@pam!proxmox_api_token_id=<your-secret>"
insecure = true
ssh {
agent = false
username = "root"
private_key = file("~/.ssh/id_ed25519")
}
}
# Upload cloud-init snippet to Proxmox
resource "proxmox_virtual_environment_file" "cloud_init" {
content_type = "snippets"
datastore_id = "local"
node_name = "homelabsvr"
source_raw {
data = file("cloud-init.yaml")
file_name = "cloud-init.yaml"
}
}
# Create the VM
resource "proxmox_virtual_environment_vm" "ubuntu_vm" {
name = "terraform-ubuntu-01"
node_name = "homelabsvr"
agent {
enabled = true
trim = true
}
clone {
vm_id = 9000
}
cpu {
cores = 4
}
memory {
dedicated = 4096
}
disk {
datastore_id = "local-lvm"
interface = "scsi0"
size = 50
}
network_device {
bridge = "vmbr0"
}
initialization {
user_data_file_id = proxmox_virtual_environment_file.cloud_init.id
ip_config {
ipv4 {
address = "192.168.1.21/24"
gateway = "192.168.1.1"
}
}
user_account {
username = "indunil"
password = "yourpassword"
keys = ["ssh-ed25519 AAAA... your-public-key"]
}
}
operating_system {
type = "l26"
}
timeout_create = 1800
}
# Wait for cloud-init to complete
resource "null_resource" "wait_for_vm" {
depends_on = [proxmox_virtual_environment_vm.ubuntu_vm]
provisioner "local-exec" {
command = <<EOT
echo "Removing old SSH host key..."
ssh-keygen -f '/home/indunil/.ssh/known_hosts' -R '192.168.1.21' || true
echo "Waiting for SSH..."
until ssh -o StrictHostKeyChecking=no \
-o ConnectTimeout=5 \
-i ~/.ssh/id_ed25519 \
indunil@192.168.1.21 'echo SSH_OK' 2>/dev/null | grep -q 'SSH_OK'; do
echo "SSH not ready yet, retrying in 10s..."
sleep 10
done
echo "SSH is up!"
echo "Waiting for cloud-init to complete..."
until ssh -o StrictHostKeyChecking=no \
-o ConnectTimeout=5 \
-i ~/.ssh/id_ed25519 \
indunil@192.168.1.21 \
'cloud-init status 2>/dev/null' | grep -q 'done'; do
echo "cloud-init still running, retrying in 15s..."
sleep 15
done
echo "VM cloud-init complete!"
EOT
}
triggers = {
vm_id = proxmox_virtual_environment_vm.ubuntu_vm.id
}
}
# Run Ansible to install IBM MQ
resource "null_resource" "ansible_mq_setup" {
depends_on = [null_resource.wait_for_vm]
provisioner "local-exec" {
command = <<EOT
ansible-playbook \
-i '192.168.1.21,' \
-u indunil \
--private-key ~/.ssh/id_ed25519 \
--ssh-extra-args='-o StrictHostKeyChecking=no' \
ibmmq-ansible/playbook.yml
EOT
}
triggers = {
vm_id = proxmox_virtual_environment_vm.ubuntu_vm.id
}
}
output "vm_ip" { value = "192.168.1.21" }
output "vm_name" { value = proxmox_virtual_environment_vm.ubuntu_vm.name }
output "mq_connection" {
value = "Channel: DEV.APP.SVRCONN | Host: 192.168.1.21 | Port: 1414"
}
Step 7 — Ansible Variables (group_vars/all.yml)
ibmmq_version: "9.3.0"
ibmmq_binary_name: "IBM_MQ_9.3.0_LINUX_X86-64.tar.gz"
ibmmq_binary_src: "~/mq-binaries/{{ ibmmq_binary_name }}"
ibmmq_install_dir: "/opt/mqm"
ibmmq_tmp_dir: "/tmp/mqinstall"
ibmmq_qmgr_name: "QM1"
ibmmq_qmgr_port: 1414
ibmmq_qmgr_channel: "DEV.APP.SVRCONN"
ibmmq_group: "mqm"
ibmmq_user: "mqm"
ibmmq_user_uid: 1001
ibmmq_group_gid: 1001
ibmmq_user_home: "/home/mqm"
ibmmq_user_shell: "/bin/bash"
ibmmq_app_user: "mqapp"
ibmmq_app_user_uid: 1002
ibmmq_app_user_home: "/home/mqapp"
ibmmq_nofile_limit: 10240
ibmmq_nproc_limit: 4096
ibmmq_data_dir: "/var/mqm"
ibmmq_log_dir: "/var/mqm/log"
Step 8 — Ansible Role Tasks
The Ansible role is split into six task files run in order.
01 — Users and Groups
Creates the mqm administrator user, mqapp application user, and their groups.
- name: Create mqm group
ansible.builtin.group:
name: "{{ ibmmq_group }}"
gid: "{{ ibmmq_group_gid }}"
state: present
- name: Create mqm user
ansible.builtin.user:
name: "{{ ibmmq_user }}"
uid: "{{ ibmmq_user_uid }}"
group: "{{ ibmmq_group }}"
home: "{{ ibmmq_user_home }}"
shell: "{{ ibmmq_user_shell }}"
create_home: true
state: present
- name: Create mqapp user
ansible.builtin.user:
name: "{{ ibmmq_app_user }}"
uid: "{{ ibmmq_app_user_uid }}"
groups: "{{ ibmmq_group }}"
home: "{{ ibmmq_app_user_home }}"
shell: /bin/bash
create_home: true
state: present
02 — System Configuration
Sets ulimits for the MQ users. The kernel parameters are already applied by cloud-init, so this step just verifies and sets PAM limits.
- name: Set system limits for mqm user
ansible.builtin.pam_limits:
domain: "{{ ibmmq_user }}"
limit_type: "{{ item }}"
limit_item: nofile
value: "{{ ibmmq_nofile_limit }}"
loop: [soft, hard]
03 — Install IBM MQ
Copies the binary, extracts it, accepts the license, and installs packages in strict dependency order using dpkg with --configure -a after each to avoid pre-dependency errors.
- name: Check if IBM MQ is already installed
ansible.builtin.command:
cmd: "/opt/mqm/bin/dspmqver"
register: mq_already_installed
changed_when: false
failed_when: false
- name: Install packages in strict dependency order
ansible.builtin.shell:
cmd: "dpkg -i {{ ibmmq_tmp_dir }}/MQServer/{{ item }} && dpkg --configure -a"
loop:
- "ibmmq-runtime_*.deb"
- "ibmmq-gskit_*.deb"
- "ibmmq-server_*.deb"
- "ibmmq-client_*.deb"
- "ibmmq-sdk_*.deb"
- "ibmmq-jre_*.deb"
- "ibmmq-java_*.deb"
- "ibmmq-man_*.deb"
- "ibmmq-samples_*.deb"
when: mq_already_installed.rc != 0
Gotcha: The
ibmmq-*package names differ from the olderMQSeries*naming convention. Also, packages must be installed one at a time withdpkg --configure -abetween each — installing them all at once causes pre-dependency failures becauseibmmq-runtimemust be fully configured before any other package can install.
04 — Environment
Sets PATH, LD_LIBRARY_PATH, and useful MQ aliases in .bashrc for both mqm and mqapp users, and writes a global /etc/profile.d/ibmmq.sh.
05 — Configure Queue Manager
Creates the queue manager, starts it, and applies the MQSC configuration using su -l mqm (the -l flag loads the full login environment which is required for MQ commands to work).
- name: Create queue manager
ansible.builtin.shell:
cmd: "su -l mqm -c 'crtmqm -p {{ ibmmq_qmgr_port }} {{ ibmmq_qmgr_name }}'"
when: qmgr_check.rc != 0
- name: Apply MQSC configuration
ansible.builtin.shell:
cmd: "su -l mqm -c 'runmqsc {{ ibmmq_qmgr_name }} < /tmp/mqsc_config.mqsc'"
The MQSC script creates:
- TCP listener on port 1414
- Server connection channel
DEV.APP.SVRCONN - Dead letter queue
- Local queues:
DEV.QUEUE.1,DEV.QUEUE.2,DEV.QUEUE.3,ADMIN.QUEUE - Channel authentication rules
06 — Systemd Service
Stops the queue manager gracefully, registers it with systemd, and starts it back up under systemd control so it auto-starts on every reboot.
- name: Stop queue manager before handing to systemd
ansible.builtin.shell:
cmd: "su -l mqm -c 'endmqm -w {{ ibmmq_qmgr_name }} 2>/dev/null || true'"
- name: Enable and start IBM MQ service via systemd
ansible.builtin.systemd:
name: "ibmmq-{{ ibmmq_qmgr_name }}"
enabled: true
state: started
daemon_reload: true
Step 9 — Run It
cd /mnt/storage/proxmox-terraform
# Install dependencies
sudo apt install -y ansible
ansible-galaxy collection install ansible.posix
# Place your MQ binary
mkdir -p ~/mq-binaries
# copy IBM_MQ_9.3.0_LINUX_X86-64.tar.gz to ~/mq-binaries/
# Deploy everything
terraform init
terraform apply
The full run takes about 7 minutes:
| Phase | Time |
|---|---|
| VM clone + boot | ~3m 42s |
| cloud-init wait | ~4s |
| Ansible (56 tasks) | ~3m 28s |
Verification
After terraform apply completes, SSH into the VM and verify:
ssh [email protected]
# Check MQ version
sudo su -l mqm -c 'dspmqver'
# Check queue manager status
sudo su -l mqm -c 'dspmq'
# QMNAME(QM1) STATUS(Running)
# Check systemd service
systemctl status ibmmq-QM1
# Check listener is up
sudo su -l mqm -c 'runmqsc QM1 <<< "DISPLAY LSSTATUS(*)"'
Key Lessons Learned
1. Proxmox API token permissions must be explicit
Even with root@pam tokens, storage and SDN paths need explicit ACL entries. Easiest fix: grant Administrator at /.
2. IBM MQ package install order matters
The ibmmq-* packages have strict pre-dependencies. Install ibmmq-runtime first, configure it with dpkg --configure -a, then install ibmmq-gskit, then the rest in order.
3. Kernel shared memory is critical
The default kernel.shmall=4096 on Ubuntu equals only 16MB of shared memory — far too low for IBM MQ. Setting it to kernel.shmmax=268435456 (256MB) in cloud-init ensures it’s ready before Ansible runs.
4. Use su -l not su - for MQ commands
su -l mqm loads the full login environment including PATH to /opt/mqm/bin. Without it, MQ commands fail.
5. Ansible become_user doesn’t work without ACL support
Ubuntu 24.04 doesn’t support the ACL chmod mode Ansible uses for privilege escalation to non-root users. Use su -l mqm -c 'command' in shell tasks instead.
Teardown
terraform destroy
This removes the VM, its disks, and the cloud-init snippet from Proxmox cleanly.
What’s Next
- Add SSH key-based auth only (disable password)
- Add TLS to the MQ listener
- Create multiple queue managers with different configs using Terraform variables
- Integrate with a monitoring stack (Prometheus + Grafana)