Ansible Automation – Deploy a VM

Another great tool to automate configurations in vSphere is Red Hat Ansible Automation Platform. Red Hat delivers two versions, a Community version and a Enterprise Version. The innovation happens in the community and the best features are perfected and added to the Enterprise version. This article uses the community version, however I would recommend your enterprise to look at the other version, as there is support available which can be really useful.

Ansible being a code language developed by a Linux developer I would say the tool works best in Unix based OS. So I am skipping showing how it is installed on Windows.

Installation on macOS

Install Homebrew (if not installed)

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

Install Python & Ansible

brew install python pip3 install ansible

Verify Installation

 ansible --version

Creating a VM on vSphere with Ansible

To create a VM on vSphere, we’ll use the community.vmware collection, which provides modules for interacting with VMware vSphere.

Install community.vmware Collection

 ansible-galaxy collection install community.vmware

Create Inventory File (inventory.yml)

all:
  hosts:
    localhost:
      ansible_connection: local

Create Playbook (create_vm.yml)

A simple playbook:

- name: Create VM on vSphere
  hosts: localhost
  gather_facts: no
  tasks:
    - name: Create a virtual machine
      community.vmware.vmware_guest:
        hostname: "vcenter.example.com"
        username: "administrator@vsphere.local"
        password: "YourPassword"
        validate_certs: no
        datacenter: "DatacenterName"
        cluster: "ClusterName"
        name: "NewVM"
        folder: "/DatacenterName/vm"
        state: powered_on
        guest_id: "otherGuest64"  # Example for a Linux VM
        disk:
          - size_gb: 20
            type: thin
            datastore: "DatastoreName"
        hardware:
          memory_mb: 4096
          num_cpus: 2
        networks:
          - name: "VM Network"
            type: dhcp
  1. Run the Playbook
ansible-playbook -i inventory.yml create_vm.yml

Now we have deployed a VM that is running.

Let us look at adding some useful parameters to the code so we can deploy a more final VM.

Deploy a production VM

Instead of using the community version of the OS and Ansible, which may work well for a development scenario but probably not so well for production, we will be using the Enterprise Editions from Red Hat.

This Ansible Playbook is designed to automate the deployment of a virtual machine (VM) running Red Hat Enterprise Linux 9, utilizing a VMware template. The VM has 4 CPUs, 16GB of RAM, and a 250GB hard drive. Networking is set up to use DHCP from VLAN 200, and an SSH key is added for secure access. Additionally, PostgreSQL is installed and configured on the VM.

Key Features:

  • Modular Structure: The playbook is structured to allow parts to be reused for other deployments.
  • Improved Error Handling: The playbook is configured to terminate if any critical steps fail.
  • PostgreSQL Installation: PostgreSQL is installed on the RHEL9 after its successful creation.
  • Service Validation: Ensures that PostgreSQL is running.
  • Port Check Validation: Confirms that PostgreSQL is listening on port 5432
  • PostgreSQL Configuration: Modifies PostgreSQL.conf to allow connections from any address and sets standard parameters.
  • Detailed Report Generation: A summary report is generated and saved at /tmp/rhel9_vm_deployment_report.txt.
  • Logging: Outputs related to the PostgreSQL installation are logged to /tmp/postgres_install.log for troubleshooting.
    • Cleanup Routine: After successful installation, the playbook removes the file /tmp/postgres_install.log. (if the installation succeeds this file is not needed)
  • Service Restart: Restarts the PostgreSQL service to apply the new configuration.

Technical Details:

  • Utilizes Red Hat Ansible Collections, including redhat_cop.vmware, ansible.posix, and community.general.
  • Ensures compatibility with enterprise-grade Ansible modules.
  • Employs dnf and subscription-manager modules for patching and registration.
  • Utilizes the systemd module for managing the PostgreSQL service.
 ---
- name: Create RHEL 9 VM on vSphere
  hosts: localhost
  gather_facts: no
  collections:
    - redhat_cop.vmware
    - ansible.posix
    - community.general

  tasks:
    - name: Check if VMware modules are installed
      ansible.builtin.assert:
        that:
          - "'redhat_cop.vmware.vmware_guest' in ansible_collections"
        fail_msg: "VMware modules not installed. Install using: ansible-galaxy collection install redhat_cop.vmware"

    - name: Create a virtual machine from Template
      redhat_cop.vmware.vmware_guest:
        hostname: "vcenter.example.com"
        username: "administrator@vsphere.local"
        password: "YourPassword"
        validate_certs: no
        datacenter: "DatacenterName"
        cluster: "ClusterName"
        name: "RHEL9VM"
        folder: "/DatacenterName/vm"
        state: powered_on
        template: "RHEL9_VMware"
        networks:
          - name: "VM Network"
            vlan: 200
            type: dhcp
        customization:
          hostname: "rhel9-vm"
          domain: "example.com"
          timezone: "Etc/UTC"

    - name: Wait for VM to be ready for SSH
      ansible.posix.wait_for:
        host: "rhel9-vm"
        port: 22
        delay: 60
        timeout: 300
        state: started

    - name: Add SSH key for firstname.lastname
      ansible.builtin.copy:
        src: /path/to/firstname.lastname.pub
        dest: /home/username/.ssh/authorized_keys
        owner: username
        mode: '0600'
      become: yes
      remote_user: root

    - name: Register the VM with Red Hat Subscription Management
      ansible.builtin.shell: |
        subscription-manager register --username=your_rh_username --password=your_rh_password
        subscription-manager attach --auto
        subscription-manager repos --enable=rhel-9-for-x86_64-baseos-rpms --enable=rhel-9-for-x86_64-appstream-rpms
      become: yes
      remote_user: root

    - name: Update the VM to the latest version
      ansible.builtin.dnf:
        name: '*'
        state: latest
      become: yes
      remote_user: root

    - name: Install PostgreSQL
      ansible.builtin.dnf:
        name:
          - postgresql-server
          - postgresql-contrib
        state: present
      become: yes
      remote_user: root

    - name: Initialize PostgreSQL Database
      ansible.builtin.command: postgresql-setup --initdb
      become: yes
      remote_user: root

    - name: Start and Enable PostgreSQL Service
      ansible.builtin.systemd:
        name: postgresql
        enabled: yes
        state: started
      become: yes
      remote_user: root

    - name: Configure PostgreSQL
      ansible.builtin.copy:
        dest: /var/lib/pgsql/data/postgresql.conf
        content: |
          # PostgreSQL Configuration
          listen_addresses = '*'
          port = 5432
          max_connections = 100
        owner: postgres
        mode: '0644'
      become: yes
      remote_user: root

    - name: Restart PostgreSQL Service
      ansible.builtin.systemd:
        name: postgresql
        state: restarted
      become: yes
      remote_user: root

    - name: Confirm PostgreSQL is listening on port 5432
      ansible.builtin.shell: |
        ss -ltn | grep ':5432'
      register: postgres_port_check
      become: yes
      remote_user: root
      failed_when: postgres_port_check.stdout == ""
      ignore_errors: no

    - name: Generate Summary Report
      ansible.builtin.copy:
        dest: /tmp/rhel9_vm_deployment_report.txt
        content: |
          RHEL 9 VM Deployment Report
          ===========================
          VM Name: RHEL9VM
          Hostname: rhel9-vm.example.com
          PostgreSQL Status: Active
          PostgreSQL Listening on Port: 5432
          Deployment Status: Successful
          Generated On: {{ ansible_date_time.date }} {{ ansible_date_time.time }}
      become: yes
      remote_user: root

    - name: Cleanup Temporary Files
      ansible.builtin.file:
        path: /tmp/postgres_install.log
        state: absent
      become: yes
      remote_user: root

    - name: Print success message
      ansible.builtin.debug:
        msg: "RHEL 9 VM successfully created from template 'RHEL9_VMware', patched, registered with Red Hat, PostgreSQL installed and validated. Report saved to /tmp/rhel9_vm_deployment_report.txt"

  handlers:
    - name: Restart VM if needed
      redhat_cop.vmware.vmware_guest_powerstate:
        hostname: "vcenter.example.com"
        username: "administrator@vsphere.local"
        password: "YourPassword"
        validate_certs: no
        datacenter: "DatacenterName"
        name: "RHEL9VM"
        state: powered_on
  execution_environment:
    name: rhel9-vm-deployment

What now?

The code above will let you deploy a VM. But we can make some improvements to the code. Lets dive into that.

Secure Password Handling

Hardcoding passwords into files that are accessible by anyone is a bad practice. Many hacks have happened due to passwords being stored on Git Hub. Ansible has a feature called Ansible Vault that would solve this for us.

First, create a separate file where we store the passwords for the services we are using in this playbook. We will call the file vars/main.yaml

# vars/main.yml
vcenter_hostname: "vcenter.example.com"
vcenter_username: "administrator@vsphere.local"
vcenter_password: "{{ vault_vcenter_password }}"
datacenter_name: "DatacenterName"
cluster_name: "ClusterName"
vm_name: "RHEL9VM"
vm_folder: "/DatacenterName/vm"
vm_template: "RHEL9_VMware"
network_name: "VM Network"
network_vlan: 200
vm_custom_hostname: "rhel9-vm"
vm_domain: "example.com"
vm_timezone: "Etc/UTC"

Then we will encrypt the file using Ansible Vault

ansible-vault encrypt vars/main.yml

When running the playbook, you’ll need to provide the vault password:

ansible-playbook playbook.yml --ask-vault-pass

Variables

A variable represents a named entity that holds data, which can be referenced and altered across playbooks and roles. They enable dynamic adjustments to values and settings in your automation scripts without needing to modify the code structure. Consequently, variables enhance flexibility and reusability, allowing for customization of tasks based on various environments and configurations inputs.

Idempotent

Ensuring idempotency in Ansible means writing playbooks and tasks such that running them multiple times will have the same effect as running them once. Idempotency is a key principle in configuration management, ensuring that systems reach and maintain a desired state without unintended side effects.

Roles

A role is a method for organizing and encapsulating related tasks, variables, files, templates, and handlers within a standardized file structure. Roles aim to simplify complex playbooks and enhance reusability by offering a framework for managing configurations and automations in a clear, modular manner.

Updated

We should consider the importance of not writing passwords in clear text but using Vault, using variables and roles to ensure that it is reusable, and ensuring that the code is idempotent.

Error Handling: Added `register`, `failed_when`, and `ignore_errors` to capture task results and handle errors gracefully.

Documentation: Added comments to describe the purpose of tasks and roles.

Idempotency Considerations:

VM Creation: The `vmware_guest` module is used to ensure the VM is in the desired state, it is checking if the VM already exists.

Package Installation: The `dnf` module with `state: present` ensures that packages are installed only if they are not already present.

Database Initialization: The `command` module utilizes the `creates` parameter to guarantee that the database is initialized only if it is not already set up.

Service Management: The `systemd` module ensures services are started and enabled.

Configuration Deployment: The `template` module deploys configuration files only if there is a change, triggering handlers when necessary.

# Directory Structure:
# roles/
# ├── rhel_vm/
# │   ├── tasks/
# │   │   ├── main.yml
# │   ├── vars/
# │   │   ├── main.yml
# │   ├── templates/
# │   ├── files/
# │   ├── handlers/
# │   │   ├── main.yml
# ├── postgresql/
# │   ├── tasks/
# │   │   ├── main.yml
# │   ├── vars/
# │   │   ├── main.yml
# │   ├── templates/
# │   │   ├── postgresql.conf.j2
# │   ├── handlers/
# │   │   ├── main.yml

# Playbook: site.yml
---
- name: Deploy RHEL 9 VM and Configure PostgreSQL
  hosts: localhost
  gather_facts: no
  roles:
    - role: rhel_vm
    - role: postgresql

# roles/rhel_vm/tasks/main.yml
---
- name: Create a virtual machine from Template
  redhat_cop.vmware.vmware_guest:
    hostname: "{{ vcenter_hostname }}"
    username: "{{ vcenter_username }}"
    password: "{{ vault_vcenter_password }}"
    validate_certs: no
    datacenter: "{{ datacenter }}"
    cluster: "{{ cluster }}"
    name: "{{ vm_name }}"
    folder: "{{ vm_folder }}"
    state: powered_on
    template: "{{ vm_template }}"
    networks:
      - name: "{{ vm_network }}"
        vlan: "{{ vm_vlan }}"
        type: dhcp
    customization:
      hostname: "{{ vm_custom_hostname }}"
      domain: "{{ vm_domain }}"
      timezone: "{{ vm_timezone }}"
  vars:
    vcenter_hostname: "vcenter.example.com"
    vcenter_username: "administrator@vsphere.local"
    datacenter: "DatacenterName"
    cluster: "ClusterName"
    vm_name: "RHEL9VM"
    vm_folder: "/DatacenterName/vm"
    vm_template: "RHEL9_VMware"
    vm_network: "VM Network"
    vm_vlan: 200
    vm_custom_hostname: "rhel9-vm"
    vm_domain: "example.com"
    vm_timezone: "Etc/UTC"
  register: vm_creation_result
  failed_when: vm_creation_result.failed
  ignore_errors: no

# roles/postgresql/tasks/main.yml
---
- name: Ensure PostgreSQL is installed
  ansible.builtin.dnf:
    name:
      - postgresql-server
      - postgresql-contrib
    state: present
  become: yes
  remote_user: root
  register: postgresql_install_result
  failed_when: postgresql_install_result.failed
  ignore_errors: no

- name: Initialize PostgreSQL Database if not already initialized
  ansible.builtin.command: postgresql-setup --initdb
  become: yes
  remote_user: root
  args:
    creates: /var/lib/pgsql/data/PG_VERSION
  register: postgresql_init_result
  failed_when: postgresql_init_result.failed
  ignore_errors: no

- name: Ensure PostgreSQL service is enabled and running
  ansible.builtin.systemd:
    name: postgresql
    enabled: yes
    state: started
  become: yes
  remote_user: root
  register: postgresql_service_result
  failed_when: postgresql_service_result.failed
  ignore_errors: no

- name: Deploy PostgreSQL configuration
  ansible.builtin.template:
    src: postgresql.conf.j2
    dest: /var/lib/pgsql/data/postgresql.conf
    owner: postgres
    mode: '0644'
  become: yes
  remote_user: root
  notify: Restart PostgreSQL
  register: postgresql_config_result
  failed_when: postgresql_config_result.failed
  ignore_errors: no

# roles/postgresql/handlers/main.yml
---
- name: Restart PostgreSQL
  ansible.builtin.systemd:
    name: postgresql
    state: restarted
  become: yes
  remote_user: root

# Use templates, handlers, and other tasks as needed

# Encrypt sensitive data using ansible-vault
# ansible-vault encrypt group_vars/all/vault.yml

    Conclusion

    In the latest example, we have seen how to maintain the state of one VM using Ansible. We can extend the code to maintain the state of several hundred or thousands of VMs. IaC tools offer a clear advantage over manual processes in this regard.

    Leave a Reply

    Your email address will not be published. Required fields are marked *

    Share on Social Media