We automate and speed up the process of setting up cloud servers with Ansible. Part 5: local_action, conditions, loops and roles

    In the first part, we started exploring Ansible, a popular tool for automating the configuration and deployment of IT infrastructure. Ansible was successfully installed in InfoboxCloud , the principles of work, basic setup are described. At the end of the article, we showed how to quickly install nginx on several servers.

    In the second part, we figured out the output of a playbook, learned how to debug and reuse Ansible scripts.

    In the third part, we learned how to write a single Ansible playbook for different operating systems (for example, with rpm and deb), how to serve hundreds of hosts and not write them all in inventory, and how to group servers by InfoboxCloud regions. We studied the use of Ansible variables and the inventory file.

    In the fourth partwe learned how to use Ansible modules to configure the server: we figured out how to run the most common scripts on remote servers in InfoboxCloud , use templating for configuration files, substituting the necessary variables, and how to use version control systems to get the code to the server.



    In this part, we will look at how to run a task locally within the playbook for remote servers, how to use conditions to perform specific tasks only in a specific situation, how to use cycles to significantly reduce the number of tasks in a playbook. In the end, we will discuss how to organize a playbook in a role.

    Run tasks locally using local_action


    Sometimes tasks need to be run on the local machine as part of the playbook execution for remote servers. For example, you can register API access keys to the cloud on the Ansible server and issue commands to the command line utility to create new cloud servers. Often, you may need to send requests to the REST API through the Ansible uri module . There is an opportunity to do something directly on the Ansible server for a separate task in the playbook, where remote servers are registered as hosts.

    Suppose you want to run a shell module on the server from where you start Ansible. To do this, the local_action option is useful , which will launch the module locally.
    ---
    - hosts: experiments
      remote_user: root
      tasks:
      - name: check running processes on remote system
        shell: ps
        register: remote_processes
      - name: remote running processes
        debug: msg="{{ remote_processes.stdout }}"
      - name: check running processes on local system
        local_action: shell ps
        register: local_processes
      - name: local running processes
        debug: msg="{{ local_processes.stdout }}"
    

    Processes on remote machines.


    Processes on the local machine.


    We see that the execution of the command is redirected to the local machine.


    That way you can run any Ansible module with local_action.

    We work with the conditions


    Ansible performs all tasks sequentially. However, for a complex playbook with dozens of tasks, you may need to run only part of the tasks depending on the situation. Earlier, we already considered the situation when , using variables, we correctly installed Apache on rpm and deb distributions. Similarly, you can specify conditions for completing tasks using when :
    ---
    - hosts: experiments
      remote_user: root
      tasks:
      - name: Install httpd package
        yum: name=httpd state=latest
        sudo: yes
        when: ansible_os_family == "RedHat"
      - name: Install apache2 package
        apt: name=apache2 state=latest
        sudo: yes
        when: ansible_os_family == "Debian"
    

    If the operating system is RedHat family - the httpd package will be installed via yum, and if the Debian family is installed - apache2 via apt. ansible_os_family - Ansible variable obtained at the gather_facts stage.

    In the playbook above, we used sudo: yes, implying that the user has sudo rights. Let's check if this is so:
    ---
    - hosts: experiments
      remote_user: root
      tasks:
      - name: Testing user sudo privilege
        command: /usr/bin/sudo -v
        register: sudo_response
        ignore_errors: yes
      - name: Stop if Users doesn`t have sudo privilege
        fail: msg="User doesn`t have sudo privilege"
        when: sudo_response.rc == 1
    



    In the example above, we ran the command on the server / usr / bin / sudo -v and saved its output to a variable via register . The output of stdout and stderr (rc, return code) was captured in the variable. In the second task, we checked the contents of the return code of the variable and if an error occurred, we should complete the playbook with the message.

    For comparison, in conditions in Ansible you can use == (equal),! = (Not equal),> (more), <(less),> = (more than equal), <= (less than equal).

    If you need to check if a variable has a character or a string, use the in and not operators.
    - name: Querying rpm list for httpd package
      shell: rpm -qa | grep httpd
      register: httpd_rpm
    - name: Check if httpd rpm is installed on the remote host
      debug: msg="httpd is installed on the remote host"
      when: "'httpd-2.2.27–1.2.x86_64' in httpd_rpm.stdout"
    – name: Check if httpd rpm is not installed on the remote host
      debug: msg="httpd is not installed on the remote host"
      when: not 'httpd-2.2.27.1.2.x86_64' in httpd_rpm.stdout
    

    You can specify multiple conditions using the and (and) and or (or) operators.
    – name: Check if httpd rpm is installed on the remote host
      debug: msg="httpd is installed on the remote host"
      when: "'httpd-2.2.27–1.2.x86_64' in httpd_rpm.stdout and 'httpd-tools-2.2.27–1.2.x86–64' in httpd_rpm.stdout"
    

    You can also check the logical value of a variable. Let's make a backup if the backup variable is set to true:
    – name: Rsync 
      shell: /usr/bin/rsync -ra /home /backup/{{ inventory_hostname}}
      sudo: yes
      when: backup
    

    Ansible allows the condition to use information about whether a variable has already been defined. To do this, use when: var is not define (where var is the name of the variable, is not define is not yet defined, is defined is already defined).

    We work with cycles


    It happens that you need to install several packages on the server at once. But writing many tasks for this can turn into a real nightmare. Using loops will solve the problem.

    Standard cycles

    Using standard loops, you can pass the list of packages for installation and Ansible will run the task for all specified packages.
    ---
    - hosts: experiments
      remote_user: root
      tasks: 
      – name: Install nginx package
        yum: name={{ item }} state=latest
        with_items:
        – nginx
        – htop
        sudo: yes
    

    In the example above, we used the "with_items:" construct to set the variables and used the default variable item. At each iteration, item takes the next value specified in with_items.



    The task starts once, but apt is called for all specified packages. You can also use with_items as a dictionary instead of strings:
    with_items:
    – {name: 'httpd', state: 'latest'}
    – {name: 'htop', state: 'absent'}
    

    Nested loops

    Nested loops are useful when you want to perform multiple operations on the same resource. For example, if you want to provide access to many databases for MySQL users.
    –––
    – hosts: experiments
      remote_user: root
      tasks:
      – name: give users access to multiple databases
      mysql_user: name={{ item[0] }} priv={{ item[1]}}.*:ALL append_privs=yes password=pass login_user=root login_password=root
      with_nested:
      – ['alexey', 'alexander']
      – ['clientdb', 'providerdb']
    

    In the above example, we use the mysql_user module to set the rights to the databases and use nested loops with two lists: a list of users and a list of databases. Ansible will launch the mysql_user module for the user alexey, give rights to all the databases listed in the second list, then start the user for alexander and also give rights.

    Subelement Cycles


    In the previous example, we assigned all the specified databases to all the specified users. But what if each user needs to assign a specific set of databases? To do this, we will need cycles for the subelements.
    ---
    - hosts: experiments
      remote_user: root
      vars:
        users:
        – name: alexey
          database:
          – clientdb
          – providerdb
        – name: alexander
          database:
          – providerdb
      tasks:
      – name: give users access to multiple databases
        mysql_user: name={{ item.0.name }} priv={{ item.1 }}.*:ALL append_privs=yes password=pass login_user=root login_password=root
        with_subelements:
        – users
        - database
    

    We have created dictionaries that consist of user names and database names. Instead of adding user data to the playbook, you can put it in a separate variable file and include it in the playbook. Ansible will go through the dictionary using the item variable. Ansible assigns numerical values ​​to keys represented by the with_subelements construct, starting with 0. In dictionary 0, name is a key-value pair, so we use item.0.name to refer to username. Dictionary is a simple list, so we use item.1 to access it.

    Working with roles


    When designing architectures, they usually operate on server roles: web server, database server, load balancer, and so on. Each role includes a specific set of software for installation and configuration. As your system grows, components that can be reused will gradually stand out. Ansible roles provide a convenient way to organize your playbook. Based on the predefined file structure, role components will be loaded. In fact, roles are just the magic around include (imports) to make preparing a playbook easier.

    Typical playbook structure with roles:
    ---
    - hosts: webservers
      roles:
         - common
         - web
         – db
    

    The file structure of the roles will look like this:
    site.yml
    webservers.yml
    roles/
       common/
         files/
         templates/
         tasks/
         handlers/
         vars/
         defaults/
         meta/
       web/
         files/
         templates/
         tasks/
         handlers/
         vars/
         defaults/
         meta/
       db/
         files/
         templates/
         tasks/
         handlers/
         vars/
         defaults/
         meta/
    

    If there is no directory in a role, it will be ignored and the playbook will be executed. You do not have to have all the elements and directories of the playbook.

    Rules used for each role:
    • If roles / x / tasks / main.yml exists, tasks will be added to the playbook runtime.
    • If roles / x / handlers / main.yml exists, event handlers will be added to the playbook runtime.
    • If roles / x / vars / main.yml exists, the variables will be added to the playbook runtime.
    • If roles / x / meta / mail.yml exists, any dependency roles will be added to the list of roles. (In meta, you can specify a list of roles that must be applied before a specific role so that it applies correctly).
    • Any copy task can refer to a file in roles / x / files without specifying an absolute or relative path.
    • Any script task can refer to scripts in roles / x / files without specifying an absolute or relative path.
    • Any template task can refer to roles / x / templates without specifying an absolute or relative path.
    • Any imported tasks can refer to task files in the roles / x / tasks directory without specifying an absolute or relative path.

    In the config file Anible, you can set roles_path (directory with roles). This may come in handy if you have a playbook in one repository and the roles themselves in another. You can specify multiple paths to roles through the colon at once:
    roles_path = /opt/mysite/roles:/opt/othersite/roles
    

    In a role, you can pass variables or use conditions:
    ---
    - hosts: experiments
      roles:
       – common
       – {role: web, dir: '/var/www', port: 80}
       – {role: repository, when: "ansible_os_family =='RedHat'"}
    

    Earlier in the articles, we did not consider tags. With their help, you can run the marked part of the playbook.
    With tasks, using tags looks like this:
    tasks:
        - apt: name={{ item }} state=installed
          with_items:
             - httpd
             - htop
          tags:
             - packages
        - template: src=templates/src.j2 dest=/var/www/.htaccess
          tags:
             - configuration
    

    You can run the playbook part like this: ansible-playbook example.yml --tags “configuration, packages” or skip the execution of the part like this: ansible-playbook example.yml --skip-tags “notification”.

    So tags can also be used when specifying roles:
    ---
    - hosts: experiments
      roles:
        - { role: web, tags: ["apache", "simple"] }
    

    You can specify which tasks should be completed before and after the role:
    ---
    - hosts: experiments
      pre_tasks:
        - shell: echo 'hello, habr'
      roles:
        - { role: web }
      tasks:
        - shell: echo 'still busy'
      post_tasks:
        - shell: echo 'goodbye, habr'
    

    Role dependencies

    Role dependencies allow you to automatically execute dependent roles when you run specific roles that have dependencies. Dependencies are stored in roles / x / meta / main.yml. Parameters can be passed along with dependent roles. The path to the roles can be specified both in abbreviated form, and in full. A version control repository can also be used.
    ---
    dependencies:
      - { role: common, some_parameter: 3 }
      - { role: '/path/to/common/roles/foo', x: 1 }
      - { role: 'git+http://git.example.com/repos/role-foo,v1.1,foo' }
    

    If the same role is specified in dependencies several times, it will start only once. If you need it several times, you can explicitly ask for it in the dependency file.

    Ansible galaxy


    Ansible Galaxy - Ansible role repository. From this resource you can use ready-made Ansible roles or add your own.

    Conclusion


    The writing of the article was greatly helped by the book Learning Ansible and of course the official documentation .

    All experiments with Ansible are conveniently carried out in InfoboxCloud , since it is possible for each virtual server to set exactly the amount of resources that is necessary for the task (CPU / Ram / disk independently of each other) or use autoscaling, rather than choosing a VM from ready-made templates. When experiments are not carried out - you can just turn off the VM and pay only the cost of the disk.

    If you find a mistake in the article, the author will gladly correct it. Please write to the PM or e-mailabout her. There you can ask questions about Ansible for coverage in subsequent articles. If you cannot write comments on Habré, you can leave them in the InfoboxCloud Community .

    Successful work!

    Also popular now: