CloudStack Cloud Guide

    Ansible contains a number of extra modules for interacting with CloudStack based clouds. All modules support check mode, are designed to be idempotent, have been created and tested, and are maintained by the community.

    Note

    Some of the modules will require domain admin or root admin privileges.

    Prerequisites

    Prerequisites for using the CloudStack modules are minimal. In addition to Ansible itself, all of the modules require the python library

    You’ll need this Python module installed on the execution host, usually your workstation.

    1. $ pip install cs

    Or alternatively starting with Debian 9 and Ubuntu 16.04:

    Note

    cs also includes a command line interface for ad-hoc interaction with the CloudStack API e.g. $ cs listVirtualMachines state=Running.

    Limitations and Known Issues

    VPC support has been improved since Ansible 2.3 but is still not yet fully implemented. The community is working on the VPC integration.

    Credentials File

    You can pass credentials and the endpoint of your cloud as module arguments, however in most cases it is a far less work to store your credentials in the cloudstack.ini file.

    The python library cs looks for the credentials file in the following order (last one wins):

    • A .cloudstack.ini (note the dot) file in the home directory.
    • A CLOUDSTACK_CONFIG environment variable pointing to an .ini file.
    • A cloudstack.ini (without the dot) file in the current working directory, same directory as your playbooks are located.

    The structure of the ini file must look like this:

    1. $ cat $HOME/.cloudstack.ini
    2. [cloudstack]
    3. endpoint = https://cloud.example.com/client/api
    4. key = api key
    5. secret = api secret
    6. timeout = 30

    Note

    The section [cloudstack] is the default section. CLOUDSTACK_REGION environment variable can be used to define the default section.

    New in version 2.4.

    The ENV variables support CLOUDSTACK_* as written in the documentation of the library cs, like e.g CLOUDSTACK_TIMEOUT, , etc. has been implemented into Ansible. It is even possible to have some incomplete config in your cloudstack.ini:

    1. $ cat $HOME/.cloudstack.ini
    2. [cloudstack]
    3. endpoint = https://cloud.example.com/client/api
    4. timeout = 30

    and fulfill the missing data by either setting ENV variables or tasks params:

    1. ---
    2. - name: provision our VMs
    3. hosts: cloud-vm
    4. tasks:
    5. - name: ensure VMs are created and running
    6. delegate_to: localhost
    7. cs_instance:
    8. api_key: your api key
    9. api_secret: your api secret
    10. ...

    Regions

    If you use more than one CloudStack region, you can define as many sections as you want and name them as you like, e.g.:

    1. $ cat $HOME/.cloudstack.ini
    2. endpoint = https://api.exoscale.ch/compute
    3. key = api key
    4. secret = api secret
    5.  
    6. [example_cloud_one]
    7. endpoint = https://cloud-one.example.com/client/api
    8. key = api key
    9. secret = api secret
    10.  
    11. [example_cloud_two]
    12. endpoint = https://cloud-two.example.com/client/api
    13. key = api key
    14. secret = api secret

    Hint

    Sections can also be used to for login into the same region using different accounts.

    By passing the argument api_region with the CloudStack modules, the region wanted will be selected.

    1. - name: ensure my ssh public key exists on Exoscale
    2. cs_sshkeypair:
    3. name: my-ssh-key
    4. public_key: "{{ lookup('file', '~/.ssh/id_rsa.pub') }}"
    5. api_region: exoscale
    6. delegate_to: localhost

    Or by looping over a regions list if you want to do the task in every region:

    1. - name: ensure my ssh public key exists in all CloudStack regions
    2. local_action: cs_sshkeypair
    3. name: my-ssh-key
    4. public_key: "{{ lookup('file', '~/.ssh/id_rsa.pub') }}"
    5. api_region: "{{ item }}"
    6. loop:
    7. - exoscale
    8. - example_cloud_one
    9. - example_cloud_two

    New in version 2.3.

    Since Ansible 2.3 it is possible to use environment variables for domain (CLOUDSTACK_DOMAIN), account (CLOUDSTACK_ACCOUNT), project (CLOUDSTACK_PROJECT), VPC (CLOUDSTACK_VPC) and zone (CLOUDSTACK_ZONE). This simplifies the tasks by not repeating the arguments for every tasks.

    1. - hosts: cloud-vm
    2. tasks:
    3. - block:
    4. - name: ensure my ssh public key
    5. cs_sshkeypair:
    6. name: my-ssh-key
    7. public_key: "{{ lookup('file', '~/.ssh/id_rsa.pub') }}"
    8.  
    9. - name: ensure my ssh public key
    10. cs_instance:
    11. display_name: "{{ inventory_hostname_short }}"
    12. template: Linux Debian 7 64-bit 20GB Disk
    13. service_offering: "{{ cs_offering }}"
    14. ssh_key: my-ssh-key
    15. state: running
    16.  
    17. delegate_to: localhost
    18. environment:
    19. CLOUDSTACK_DOMAIN: root/customers
    20. CLOUDSTACK_PROJECT: web-app
    21. CLOUDSTACK_ZONE: sf-1

    Note

    You are still able overwrite the environment variables using the module arguments, e.g. zone: sf-2

    Note

    Unlike CLOUDSTACK_REGION these additional environment variables are ignored in the CLI cs.

    Use Cases

    The following should give you some ideas how to use the modules to provision VMs to the cloud. As always, there isn’t only one way to do it. But as always: keep it simple for the beginning is always a good start.

    Our CloudStack cloud has an advanced networking setup, we would like to provision web servers, which get a static NAT and open firewall ports 80 and 443. Further we provision database servers, to which we do not give any access to. For accessing the VMs by SSH we use a SSH jump host.

    This is how our inventory looks like:

    1. webserver
    2. db-server
    3. jumphost
    4.  
    5. [webserver]
    6. web-01.example.com public_ip=198.51.100.20
    7. web-02.example.com public_ip=198.51.100.21
    8.  
    9. [db-server]
    10. db-01.example.com
    11. db-02.example.com
    12.  
    13. [jumphost]
    14. jump.example.com public_ip=198.51.100.22

    As you can see, the public IPs for our web servers and jumphost has been assigned as variable directly in the inventory.

    The configure the jumphost, web servers and database servers, we use group_vars. The group_vars directory contains 4 files for configuration of the groups: cloud-vm, jumphost, webserver and db-server. The cloud-vm is there for specifying the defaults of our cloud infrastructure.

    cs_offering: Smallcs_firewall: []

    Our database servers should get more CPU and RAM, so we define to use a Large offering for them.

    1. # file: group_vars/db-server

    cs_offering: Large

    The web servers should get a Small offering as we would scale them horizontally, which is also our default offering. We also ensure the known web ports are opened for the world.

    1. # file: group_vars/webserver

    cs_firewall:

    • { port: 80 }
    • { port: 443 }

    Further we provision a jump host which has only port 22 opened for accessing the VMs from our office IPv4 network.

    cs_firewall:

    • { port: 22, cidr: "17.17.17.0/24" }

    Now to the fun part. We create a playbook to create our infrastructure we call it infra.yml:

    1. # file: infra.yaml

    • name: provision our VMshosts: cloud-vmtasks:

      • name: run all enclosed tasks from localhostdelegate_to: localhostblock:

        • name: ensure VMs are created and runningcs_instance: name: "{{ inventory_hostname_short }}" template: Linux Debian 7 64-bit 20GB Disk service_offering: "{{ cs_offering }}" state: running

        • name: ensure firewall ports openedcs_firewall: ip_address: "{{ public_ip }}" port: "{{ item.port }}" cidr: "{{ item.cidr | default('0.0.0.0/0') }}"loop: "{{ cs_firewall }}"when: public_ip is defined

        • name: ensure static NATscs_staticnat: vm="{{ inventory_hostname_short }}" ip_address="{{ public_ip }}"when: public_ip is defined

    In the above play we defined 3 tasks and use the group cloud-vm as target to handle all VMs in the cloud but instead SSH to these VMs, we use delegate_to: localhost to execute the API calls locally from our workstation.

    In the first task, we ensure we have a running VM created with the Debian template. If the VM is already created but stopped, it would just start it. If you like to change the offering on an existing VM, you must add force: yes to the task, which would stop the VM, change the offering and start the VM again.

    In the third task we add static NAT to the VMs having a public IP defined.

    Note

    The public IP addresses must have been acquired in advance, also see cs_ip_address

    Note

    For some modules, e.g. you usually want this to be executed only once, not for every VM. Therefore you would make a separate play for it targeting localhost. You find an example in the use cases below.

    Use Case: Provisioning on a Basic Networking CloudStack setup

    A basic networking CloudStack setup is slightly different: Every VM gets a public IP directly assigned and security groups are used for access restriction policy.

    This is how our inventory looks like:

    1. [cloud-vm:children]
    2. webserver
    3.  
    4. [webserver]
    5. web-01.example.com
    6. web-02.example.com

    The default for your VMs looks like this:

    1. # file: group_vars/cloud-vm

    cs_offering: Smallcs_securitygroups: [ 'default']

    Our webserver will also be in security group web:

    1. # file: group_vars/webserver

    cs_securitygroups: [ 'default', 'web' ]

    The playbook looks like the following:

    1. # file: infra.yaml

    • name: cloud base setuphosts: localhosttasks:

      • name: upload ssh public keycs_sshkeypair: name: defaultkey public_key: "{{ lookup('file', '~/.ssh/id_rsa.pub') }}"

      • name: ensure security groups existcs_securitygroup: name: "{{ item }}"loop:

        • default
        • web
      • name: add inbound SSH to security group defaultcs_securitygroup_rule: security_group: default start_port: "{{ item }}" end_port: "{{ item }}"loop:

        • 22
      • name: add inbound TCP rules to security group webcs_securitygroup_rule: security_group: web start_port: "{{ item }}" end_port: "{{ item }}"loop:

        • 80
        • 443
    • name: install VMs in the cloudhosts: cloud-vmtasks:

      • delegate_to: localhostblock:

        • name: create and run VMs on CloudStackcs_instance: name: "{{ inventory_hostname_short }}" template: Linux Debian 7 64-bit 20GB Disk service_offering: "{{ cs_offering }}" security_groups: "{{ cs_securitygroups }}" ssh_key: defaultkey state: Runningregister: vm

        • name: show VM IPdebug: msg="VM {{ inventory_hostname }} {{ vm.default_ip }}"

        • name: waiting for SSH to come upwait_for: port=22 host={{ vm.default_ip }} delay=5

    In the first play we setup the security groups, in the second play the VMs will created be assigned to these groups. Further you see, that we assign the public IP returned from the modules to the host inventory. This is needed as we do not know the IPs we will get in advance. In a next step you would configure the DNS servers with these IPs for accessing the VMs with their DNS name.