This guide walks you through setting up Morpheus 8 to provision virtual machines both on-prem (vSphere, KVM, Hyper-V) and in the cloud (AWS, Azure, etc.), and to automatically configure them using Ansible (Standalone mode) running from the Morpheus appliance.
Ensure your VM image (on-prem or cloud) includes:
- OS: Ubuntu, CentOS, RHEL, etc.
- User: A non-root user (e.g.
ubuntu
,morpheus
,centos
) - SSH Access:
- Public key added to
~/.ssh/authorized_keys
sshd
enabled
- Public key added to
- Sudo Access:
- Passwordless sudo configured:
echo "morpheus ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/morpheus
- Passwordless sudo configured:
Navigate to: Infrastructure > Clouds > + Add
- For on-prem:
- VMware vCenter
- KVM (libvirt)
- Hyper-V
- For cloud:
- AWS, Azure, GCP, etc.
- Provide access credentials, resource pools, networks
Navigate to: Infrastructure > Credentials > + Add
- Type: SSH Key or Username/Password
- Username: User in your base image
- Private Key: Paste SSH private key
- Save and assign to your Cloud or Instance Layout
Go to: Library > Instance Types > + Add
- Name:
Linux-Base
(example) - Create a Layout:
- Select Cloud, image/template, size
- Assign credential and provisioning method (Cloud-Init if supported)
Navigate to: Admin > Integrations > + Add
- Type: Ansible
- Mode: Standalone
- Executable Path:
/usr/bin/ansible-playbook
- Save the integration
Navigate to: Library > Templates > Scripts > + Add
- Name:
Configure Webserver
- Type: Ansible Playbook
- Upload a
.yml
file or ZIP withsite.yml
- Choose your Ansible integration
Navigate to: Admin > Integrations > Code Repositories > + Add
- Add your Git repo (public or private)
- Morpheus syncs your playbooks automatically
You can use a flat playbook or a role-based layout. Example below:
---
- name: Configure VM post-provision
hosts: all
become: yes
vars:
ansible_python_interpreter: /usr/bin/python3
roles:
- webserver
---
- name: Install NGINX
apt:
name: nginx
state: present
when: ansible_os_family == "Debian"
- name: Start NGINX
service:
name: nginx
state: started
enabled: yes
server {
listen 80;
server_name {{ inventory_hostname }};
root /var/www/html;
}
Go to: Library > Automation > Tasks > + Add
- Type: Ansible Playbook
- Select uploaded
.yml
or synced playbook - Choose the correct Ansible integration
Go to: Library > Automation > Workflows > + Add
- Type: Provisioning Workflow
- Add the Ansible Task you just created
Attach the workflow to:
- Instance Layout
- Blueprint
- Or manually during provisioning
Go to: Library > Instance Types > [Layout] > Automation Tab
Attach the Provisioning Workflow
Provision a new VM using your configured Instance Type. Then:
- Navigate to:
Instances > [Your VM] > History
- View the Automation Logs to confirm Ansible executed successfully
Create a Cloud-Init script to inject keys and users at build time:
#cloud-config
users:
- name: morpheus
sudo: ALL=(ALL) NOPASSWD:ALL
groups: sudo
shell: /bin/bash
ssh_authorized_keys:
- ssh-rsa AAAAB3Nza...your_key_here...
disable_root: true
ssh_pwauth: false
Upload at: Library > Templates > Scripts > + Add
Type: Cloud-Init
Attach to your Layoutβs Provisioning Scripts section
Step | Action |
---|---|
β | Prepare image with SSH, user, sudo |
β | Add Cloud (vSphere, AWS, etc.) |
β | Create and assign Credentials |
β | Set up Ansible Standalone Integration |
β | Upload or sync Ansible Playbooks |
β | Create Ansible Task & Workflow |
β | Attach Workflow to Layout or Blueprint |
β | Provision VM and confirm automation |
Add this task to print host info during provisioning:
- name: Debug host info
debug:
msg: "Hostname: {{ inventory_hostname }}, IP: {{ ansible_host }}, Cloud: {{ cloud_name | default('N/A') }}"
get_pure_volumes
- name: Connect to Pure Storage and list volumes
hosts: localhost
gather_facts: no
vars_prompt:
- name: "purefa_username"
prompt: "Enter your Pure Storage username"
private: no
- name: "purefa_password"
prompt: "Enter your Pure Storage password"
private: yes
- name: "purefa_ip"
prompt: "Enter the IP of the Pure Storage array"
private: no
- name: "api_version"
prompt: "Enter the Pure API version (e.g., 2.2)"
private: no
tasks:
- name: Authenticate with Pure Storage and get Bearer token
uri:
url: "https://{{ purefa_ip }}/api/{{ api_version }}/auth/session"
method: POST
user: "{{ purefa_username }}"
password: "{{ purefa_password }}"
force_basic_auth: yes
validate_certs: no
status_code: 200
return_content: yes
headers:
Content-Type: "application/json"
register: auth_response
- name: Extract the bearer token
set_fact:
bearer_token: "{{ auth_response.json.session.id }}"
- name: Save bearer token to file
copy:
content: "{{ bearer_token }}"
dest: "./pure_token.txt"
mode: '0600'
- name: Get volume info using the bearer token
uri:
url: "https://{{ purefa_ip }}/api/{{ api_version }}/volumes"
method: GET
headers:
Authorization: "Bearer {{ bearer_token }}"
validate_certs: no
return_content: yes
register: volumes_response
- name: Show volume info
debug:
var: volumes_response.json
[defaults]
inventory = localhost,
collections_paths = ./collections
host_key_checking = False
retry_files_enabled = False
deprecation_warnings = False
timeout = 30
stdout_callback = yaml
interpreter_python = auto_silent
[privilege_escalation]
become = False
[ssh_connection]
pipelining = True
list_volumes.yml
- name: List all volumes on the FlashArray
hosts: localhost
gather_facts: false
collections:
- purestorage.flasharray
tasks:
- name: Load API token and array IP
include_vars:
file: "./vars/pure_connection.yml"
- name: Get volume info
purefa_volume_info:
api_token: "{{ api_token }}"
fa_url: "{{ fa_url }}"
register: volume_info
- name: Display volume names
debug:
msg: "{{ item.name }}"
loop: "{{ volume_info.volumes }}"
list_hosts_and_wwns.yml
- name: List hosts and their WWNs
hosts: localhost
gather_facts: false
collections:
- purestorage.flasharray
tasks:
- name: Load API token and array IP
include_vars:
file: "./vars/pure_connection.yml"
- name: Get host info
purefa_host_info:
api_token: "{{ api_token }}"
fa_url: "{{ fa_url }}"
register: host_info
- name: Show each host's WWNs
debug:
msg: "Host: {{ item.name }}, WWNs: {{ item.wwn }}"
loop: "{{ host_info.hosts }}"
list_protection_groups.yml
- name: List protection groups and member volumes
hosts: localhost
gather_facts: false
collections:
- purestorage.flasharray
tasks:
- name: Load API token and array IP
include_vars:
file: "./vars/pure_connection.yml"
- name: Get pgroup info
purefa_pgroup_info:
api_token: "{{ api_token }}"
fa_url: "{{ fa_url }}"
register: pgroup_info
- name: Show pgroup members
debug:
msg: "PGroup: {{ item.name }}, Volumes: {{ item.volumes }}"
loop: "{{ pgroup_info.pgroups }}"
get_array_info.yml
- name: Get FlashArray system info
hosts: localhost
gather_facts: false
collections:
- purestorage.flasharray
tasks:
- name: Load API token and array IP
include_vars:
file: "./vars/pure_connection.yml"
- name: Get array info
purefa_array_info:
api_token: "{{ api_token }}"
fa_url: "{{ fa_url }}"
register: array_info
- name: Show array capacity and model
debug:
msg: "Model: {{ array_info.arrays[0].model }}, Capacity: {{ array_info.arrays[0].capacity }}"
get_bearer_token.yml
- name: Authenticate with Pure Storage and save token + IP
hosts: localhost
gather_facts: false
vars_prompt:
- name: "purefa_username"
prompt: "Enter your Pure Storage username"
private: no
- name: "purefa_password"
prompt: "Enter your Pure Storage password"
private: yes
- name: "purefa_ip"
prompt: "Enter the IP of the Pure Storage array"
private: no
- name: "api_version"
prompt: "Enter the Pure API version (e.g., 2.2)"
private: no
tasks:
- name: Authenticate and get Bearer token
uri:
url: "https://{{ purefa_ip }}/api/{{ api_version }}/auth/session"
method: POST
user: "{{ purefa_username }}"
password: "{{ purefa_password }}"
force_basic_auth: yes
validate_certs: no
status_code: 200
return_content: yes
headers:
Content-Type: "application/json"
register: auth_response
- name: Extract bearer token
set_fact:
bearer_token: "{{ auth_response.json.session.id }}"
- name: Ensure vars directory exists
file:
path: "./vars"
state: directory
mode: '0755'
- name: Save token and IP address to a vars file
copy:
content: |
api_token: "{{ bearer_token }}"
fa_url: "https://{{ purefa_ip }}"
dest: "./vars/pure_connection.yml"
mode: '0600'
- name: Confirm saved info
debug:
msg: "Saved bearer token and array IP to vars/pure_connection.yml"
dummy file!!
pure_connection.yml
api_token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.DUMMYTOKENVALUE.abc123
fa_url: https://10.99.55.14