Ansible - Create OpenStack servers with Ansible 2.0 and the os_server module and a dynamic inventory

10-09-2016 | Remy van Elst


Table of Contents


I regularly deploy clusters and single servers on OpenStack with Ansible. However, Ansible 2.0 comes with new OpenStack modules my playbooks still used the old ones. I reserved some time to convert these playbooks to the new modules and ansible 2. This article shows a very simple example, it creates three servers in OpenStack and adds them to different hostgroups based on variables. For example, to create one loadbalancer and two appservers and run specific playbooks on those hosts based on their role.

Since Ansible 2 the OpenStack modules are renewed. The old nova_* modules are replaced by the os_server modules. Ansible 2 also changed some deperecated stuff regarding yaml parsing and variable concatination. Therefore some of my old playbooks were not working anymore and I had to figure out how to get it working with the new versions.

I've written about Ansible before, click that link to get all the articles about Ansible.

ansible

If you like this article, consider sponsoring me by trying out a Digital Ocean VPS. With this link you'll get a $5 VPS for 2 months free (as in, you get $10 credit). (referral link)

I found documentation on the new modules but that just created a single instance. I need to spawn multiple instances and add those to specific hostgroups based on variables, so this guide wasn't complete enough for me.

This is my folder structure:

$ tree -I "*.git"
.
|-- ansible.cfg
|-- group_vars
|   `-- all.yml
|-- roles
|   |-- haproxy
|   |   |-- handlers
|   |   |   `-- main.yml
|   |   |-- tasks
|   |   |   `-- main.yml
|   |   |-- templates
|   |   |   `-- haproxy.cfg.j2
|   |   `-- vars
|   |       `-- main.yml
|   `-- create_instances
|       |-- tasks
|       |   `-- main.yml
|       `-- vars
|           `-- main.yml
`-- site.yml

site.yml is the main playbook and create-instances and appservers are the specific roles. Create the folder structure if you're starting from zero.

On the local host you need the OpenStack tools and some python modules installed. The os_server page lists all the requirements. You also need an openstackrc file with credentials in your environment:

$ cat openstackrc
export OS_AUTH_URL="https://identity.stack.cloudvps.com/v2.0"
export OS_USERNAME="username"
export OS_PASSWORD="password"
export OS_TENANT_ID="UUID"
export OS_TENANT_NAME="UUID"

Source it before running the playbooks:

source openstackrc

Main playbook

The site.yml playbook first runs on locahost and creates the OpenStack instances. It also adds them to the specific hostgroups. Those hostgroups are only available when you run this playbook.

Then it starts a new play (if that is how you call multiple runs) to the first newly created hostgroup (loadbalancers) and you can add a new play to run on the appservers as well.

---
- name: create instances
  hosts: localhost
  roles:
    - { role: create_instances }


- name: deploy haproxy
  hosts: loadbalancers
  user: root
  roles:
    - { role:  haproxy }
    - { role:  keepalived }

Instance creation playbook

The first playbook, roles/create-instances/tasks/main.yml runs on localhost and creates the OpenStack instances:

    ---
    - name: launch instances
      os_server:
        name: "{{ prefix }}-{{ item.name }}"
        state: present
        key_name: "{{ item.key }}"
        availability_zone: "{{ item.availability_zone }}"
        nics: "{{ item.nics }}"
        image: "{{ item.image }}"
        flavor: "{{ item.flavor }}"
      with_items: "{{ servers }}"
      register: "os_hosts"


    - name: add hosts to inventory
      add_host:
        name: "{{ item['openstack']['human_id'] }}"
        groups: "{{ item['item']['meta']['group'] }}"
        ansible_host: "{{ item.openstack.accessIPv4 }}"
      with_items: "{{ os_hosts.results }}"

Based on the name we defined and the group we gave it also is adds the hosts to a new hostgroup. That hostgroup is only active within this playbook run.

The parameters we used to create the instances are also available in the result of that action. It's a dict, so you can access all three the servers we created. That is were we get the group value from.

If you're using a jumphost and other machines with private IP's you need to use a different ansible_ssh_host, but you can achieve that by adding extra data to the variables. The debug module is your friend here.

If the instances are already created, it will not create them again but it will add then to the hostgroup again.

The variables required for this playbook are the following (roles/create-instances/vars/main.yml):

---
prefix: demo
servers:
  - name: lb1
    image: CloudVPS Ubuntu 16.04
    flavor: Standard 2
    key: SSH-Key
    nics: "net-id=00000000-0000-0000-0000-000000000000"
    availability_zone: NL1
    meta: 
      group: loadbalancers
  - name: app1
    image: CloudVPS Ubuntu 16.04
    flavor: Standard 2
    key: SSH-Key
    nics: "net-id=00000000-0000-0000-0000-000000000000"
    availability_zone: NL1
    meta: 
      group: appservers
  - name: app2
    image: CloudVPS Ubuntu 16.04
    flavor: Standard 2
    key: SSH-Key
    nics: "net-id=00000000-0000-0000-0000-000000000000"
    availability_zone: NL2
    meta: 
      group: appservers

The prefix is used in the servername and can be used to create different pseudo-groups in the same tenant/project. I do recommend to create different projects/tenants per environment (accept/staging etc) instead of prefixes, since you can then manage the rights more fine grained.

Role specific playbooks

The role specific playbooks are just regular playbook roles you would run. For the guide you can use an example play which just does a ping (roles/ping/tasks/main.yml):

---
- name: ping instances
  ping:

The full play then results in:

    $ ansible-playbook site.yml 

     [WARNING]: provided hosts list is empty, only localhost is available

    PLAY [create instances] ********************************************************

    TASK [setup] *******************************************************************
    ok: [localhost]

    TASK [create_instances : launch instances] *************************************
    changed: [localhost] => (item={u'name': u'lb1', u'availability_zone': u'NL1', u'nics': u'net-id=00000000-0000-0000-0000-000000000000', u'image': u'CloudVPS Ubuntu 16.04', u'meta': {u'group': u'loadbalancers'}, u'key': u'SSH-Key', u'flavor': u'Standard 2'})
    changed: [localhost] => (item={u'name': u'app1', u'availability_zone': u'NL1', u'nics': u'net-id=00000000-0000-0000-0000-000000000000', u'image': u'CloudVPS Ubuntu 16.04', u'meta': {u'group': u'appservers'}, u'key': u'SSH-Key', u'flavor': u'Standard 2'})
    changed: [localhost] => (item={u'name': u'app2', u'availability_zone': u'NL1', u'nics': u'net-id=00000000-0000-0000-0000-000000000000', u'image': u'CloudVPS Ubuntu 16.04', u'meta': {u'group': u'appservers'}, u'key': u'SSH-Key', u'flavor': u'Standard 2'})

    TASK [create_instances : add hosts to inventory] *******************************
    changed: [localhost] => (item={u'changed': True, '_ansible_no_log': False, '_ansible_item_result': True, u'server': 
    [...] # a lot of json
    u'volumes': [], u'metadata': {}, u'human_id': u'demo-app2'}, u'id': u'eff00345-977f-4c72-4684-4aa22d1dfc9f'})

    PLAY [ping instances] **********************************************************

    TASK [setup] *******************************************************************
    ok: [demo-app1]
    ok: [demo-app2]

    TASK [common : ping instances] *************************************************
    ok: [demo-app1]
    ok: [demo-app2]

    PLAY RECAP *********************************************************************
    demo-app1            : ok=2    changed=0    unreachable=0    failed=0   
    demo-app2            : ok=2    changed=0    unreachable=0    failed=0   
    localhost            : ok=3    changed=2    unreachable=0    failed=0   

Tags: ansible, cloudvps, nova, openstack, python,