Work with Ansible - tasks with several unknowns

Googled information on Ansible , came across an article on Habré. I read and was very surprised: you can make it more beautiful! If you are interested - welcome to kat!

I must say right away that this all works on a real project (the number of nodes is more than 80, but less than 100).

Here are the tricky tasks with unknowns that at least every third, or maybe second DevOps, encounters.
  1. As a parameter of the configuration file, use previously unknown host addresses, and these nodes belong to another role ( solution )
  2. Generate inventory from unknown host addresses dynamically - by accessing AWS services ( solution )
  3. Create a Nginx configuration file, in which unknown addresses of the backend nodes must be registered in advance ( solution )

So first is the simplest:

Node Address Substitution


Let's take an example: we have a group of nodes “app_nodes”, on which a certain application is installed using a task file like this:

application.yml
---
- hosts: app_nodes
  gather_facts: yes
  roles:
    - role: common
    - role: application


Imagine that we have a ZooKeeper service, and in the application configuration file you need to register the settings for working with this service: It’s clear that with pens, write the addresses of all, for example, five nodes, on each of which ZooKeeper is installed, on the nodes with the application - no of pleasure. What can be done so that Ansible inserts all the addresses of these nodes into the configuration file template? Nothing special. Ansible uses the Jinja2 template engine on top of YAML, so we use the template engine cycle:
zk.connect=127.0.0.1:2181,127.0.0.2:2181,127.0.0.3:2181,127.0.0.4:2181, 127.0.0.5:2181



zk.connect={% for host in groups['zk_nodes'] %}{{ hostvars[host]['ansible_eth0']['ipv4']['address'] }}:{{ zk_port }}, {% endfor %}

As a result, after the template engine has worked, the desired line should be obtained, but here is the problem: we work with app_nodes nodes, and we use information (“facts”) about zk_nodes nodes in this template. How to get the addresses of zk_nodes nodes if in this job file we don’t work with these nodes at all? Assign this specific group of nodes (zk_nodes) an empty list of tasks:

application.yml
---
- hosts: zk_nodes
  gather_facts: yes
  tasks: []
- hosts: app_nodes
  gather_facts: yes
  roles:
    - role: common
    - role: application


For this job file to work, the necessary node groups must be defined in the inventory file:

environments / test / inventory
[zk_nodes]
127.0.0.1
127.0.0.2
127.0.0.3
127.0.0.4
127.0.0.5
[app_nodes]
127.0.0.10
127.0.0.11
127.0.0.12

But what if host addresses are not known in advance? For example - are virtual machines used in EC2? Here we smoothly proceed to answer the second question.

Work with dynamic inventory


There is not too much information on this topic on the Internet - as I understand it, in view of the existence of Ansible Tower.

So, you have a number of nodes in EC2, and you want to manage them centrally and easily. One possible solution is to use Ansible + a dynamic inventory script.

Here is the structure of the corresponding inventory directory:

tree ./environments/dynamic
.
├── ec2.ini
├── ec2.py
├── groups
└── group_vars
    ├── all
    ├── zk_nodes
    ├── proxies
    └── app_nodes


Here: ec2.py - the script itself, it can be taken here , the link is direct; ec2.ini - the file with the script settings, it can be taken here , the link is direct; groups - a file that describes the groups of nodes you are going to work with in this inventory, group_vars - a directory containing the values ​​of variables for each specific group, as well as common to all.

Next, under the spoiler, those settings that differ from the ini-file in the link:

Changed ec2.ini options
#регионы, где находятся наши узлы
regions=us-east-5, us-west-2
#работаем только с «живыми» (running) узлами
instance_states=running
#все параметры group_by_.... закомментированы, кроме одного:
group_by_tag_keys=true
#Отбираем, какие именно узлы войдут в наш inventory. В данном случае — те, у которых прописан тег «Name» с указанными значениями.
instance_filters = tag:Name=zk_node, tag:Name=app_node, tag:Name=proxy

In order for Ansible to correctly recognize these groups and nodes, we write the groups file :

groups file
[all:children]
zk_nodes
proxies
app_nodes
[zk_nodes:children]
tag_Name_zk_node
[proxies:children]
tag_Name_proxy
[app_nodes:children]
tag_Name_app_node
[tag_Name_zk_node]
[tag_Name_proxy]
[tag_Name_app_node]


Here we inform Ansible that the group of nodes all , with which it works by default, contains nested groups zk_nodes , proxies , app_nodes . We further inform that these groups also have nested groups that are formed dynamically, and therefore nodes are not indicated at all in their descriptions. Such a black magic - during its work, the dynamic inventory script will create groups of the form tag_ <tag name> _ <tag value> and fill these groups with nodes, and then you can work with these groups using the usual Ansible tools.

Yes, right about the group_vars directory . It is automatically read by Ansible when loading inventory, and each group in this inventory gets the variables from the file group_vars / group_name. One example of use is a separate key for a specific group of nodes:

group_vars / zk_nodes
ansible_ssh_private_key_file: "/tmp/key.pem"

Having examined the dynamic inventory, we can elegantly solve the third problem:

Nginx configuration file generation


It is clear that the Nginx configuration template does not surprise anyone on Habré, therefore I will limit myself to the upstream block with explanations.

nginx.conf upstream
upstream app_nodes {
{% for item in groups['app_nodes'] %} server {{ hostvars[item]['ec2_private_ip_address'] }}:{{ app_port}};
{% endfor %}
keepalive 600;
}

This configuration block defines a group of upstream servers with different addresses and a common port number.
The template engine will run through all the nodes of the app_nodes group, generating a line for each node. It will turn out like this
Result Example
upstream app_nodes {
127.0.0.1:3000;
127.0.0.2:3000;
127.0.0.3:3000;
127.0.0.4:3000;
keepalive 600;
}


This is different from the situation with the first solution in the absence of the need to additionally refer to the app_nodes node group with an empty list of tasks - this group is automatically created, among others, according to the groups file above, thanks to the dynamic inventory script. Well and, of course, the appeal to internal addresses of VPC is used.


Afterword


The names of environments, tasks, inventory, nodes, IP-addresses are replaced with fictitious. Any matches are random. Where file or directory names are important for the functionality, explanations are given why they are called that way.

Remember, you and only you yourself are responsible for the state of your projects to the manager, customer and your own conscience. I do not pretend to be complete. I hope this article saves someone time and pushes someone in the right direction. To the best of my ability and my understanding, I will answer questions in the comments.

Also popular now: