System approach to variables in Ansible

ansible devops codestyle

Hey! My name is Denis Kalyuzhny I work as an engineer in the development process automation department. Every day, new application builds are rolled out to hundreds of campaign servers. And in this article, I share my experience of using Ansible for these purposes.

This guide offers a way to organize variables in a deployment. This guide is designed for those who already use roles in their playbooks and read Best Practicesbut running into similar problems:

  • Having found a variable in the code, it is impossible to immediately understand what it is responsible for;
  • There are several roles, and the variables need to be associated with one value, but it doesn’t work;
  • Having difficulty explaining to others how the logic of the variables in your playbooks works

We encountered these problems on projects in our company, as a result of which we came to the rules for formatting variables in our playbooks, which to some extent solved these problems.

System approach to variables in Ansible

Variables in roles

A Role is a separate Deployment System Object. Like any object of the system, it must have an interface for interacting with the rest of the system. Role variables are such an interface.

Take, for example, the role api, which installs a Java application on the server. What variables does it have?

System approach to variables in Ansible

Role variables can be divided into 2 types by type:

1. Свойства
    a) независимые от среды
    б) зависимые от среды
2. Связи
    a) слушатели 
    б) запросы внутри системы
    в) запросы в среду

Variable properties are variables that define the behavior of a role.

Query Variables are variables whose value is used to designate resources external to the role.

Variable Listeners are variables whose value is used to form query variables.

On the other hand, 1a, 2a, 2b are variables that do not depend on the environment (iron, external resources, etc.) and can be filled with default values ​​in the defaults role. However, variables like 1.b and 2.c cannot be filled with values ​​other than 'example', since they will change from stand to stand depending on the environment.

code style

  • The name of the variable must begin with the name of the role. This will make it easy to figure out in the future what role the variable is from and what it is responsible for.
  • When using variables in roles, you must be sure to follow the principle of encapsulation and use variables defined either in the role itself or in the roles on which the current one depends.
  • Avoid using dictionaries for variables. Ansible doesn't allow you to conveniently override individual values ​​in a dictionary.

    An example of a bad variable:

    myrole_user:
        login: admin
        password: admin

    Here, login is the median variable, and password is the dependent variable. But
    since they are combined into a dictionary, you will have to specify it in full
    Always. Which is very inconvenient. Better this way:

    myrole_user_login: admin
    myrole_user_password: admin

Variables in deployment playbooks

When compiling a deployment playbook (hereinafter referred to as a playbook), we adhere to the rule that it should be placed in a separate repository. Just like roles: each in its own git repository. This allows you to realize that the roles and the playbook are different independent objects of the deployment system, and changes in one object should not affect the operation of the other. This is achieved by changing the default values ​​of the variables.

When compiling a playbook, to summarize, it is possible to override the default values ​​of role variables in two places: in playbook variables and in inventory variables.

mydeploy                        # Каталог деплоя
├── deploy.yml                  # Плейбук деплоя
├── group_vars                  # Каталог переменных плейбука
│   ├── all.yml                 # Файл для переменных связи всей системы
│   └── myapi.yml               # Файл переменных свойств группы myapi
└── inventories                 #
    └── prod                    # Каталог окружения prod
        ├── prod.ini            # Инвентори файл
        └── group_vars          # Каталог для переменных инвентори
            └── myapi           #
                ├── vars.yml    # Средозависимые переменные группы myapi
                └── vault.yml   # Секреты (всегда средозависимы) *

* - Variables and Vaults

The difference is that playbook variables are always used when calling playbooks located at the same level with it. This means that these variables are great for changing the default values ​​of variables that do not depend on the environment. Conversely, inventory variables will only be used for a particular environment, which is ideal for environment-specific variables.

It is important to note that variable precedence will not allow you to redefine variables first in playbook variables and then separately in the same inventory.

This means that already at this stage you need to decide whether the variable is environment-dependent or not and place it in the right place.

For example, in one project, the variable responsible for enabling SSL was environment-dependent for a long time, since we could not enable SSL for reasons beyond our control at one of the stands. After we fixed this issue, it became medium independent and moved to playbook variables.

Property Variables for Groups

Let's expand our model in Figure 1 by adding 2 groups of servers with a different Java application, but with different settings.

System approach to variables in Ansible

Imagine what the playbook will look like in this case:

- hosts: myapi
  roles:
    - api

- hosts: bbauth
  roles:
    - auth

- hosts: ghauth
  roles:
    - auth

We have three groups in the playbook, so it's recommended to create as many group files in group_vars inventory variables and playbook variables at once. One group file in this case is the description of one component of your application in the playbook. When you open the group file in the playbook variables, you immediately see all the differences from the default behavior of the roles assigned to the group. In inventory variables: differences in group behavior from booth to booth.

code style

  • Try not to use host_vars variables at all, as they do not describe the system, but only a special case, which in the long run will lead to questions: "Why is this host different from the rest?", The answer to which is not always easy to find.

Link variables

However, that's about property variables, but what about link variables?
Their difference is that they must have the same value in different groups.

At the beginning there was idea use a monstrous construction of the form:
hostvars[groups['bbauth'][0]]['auth_bind_port'], but it was immediately abandoned
because it has flaws. First, the bulkiness. Secondly, dependence on a specific host in the group. Thirdly, it is necessary to collect facts from all hosts before starting the deployment, if we do not want to get an undefined variable error.

As a result, it was decided to use link variables.

Link variables are variables that belong to the playbook and are needed to link system objects.

Link variables are populated in general system variables group_vars/all/vars and are formed by removing all listener variables from each group, and adding the name of the group from which the listener was removed to the beginning of the variable.

Thus, the uniformity and non-intersection of names is ensured.

Let's try to bind variables from the example above:

System approach to variables in Ansible

Imagine that we have variables that depend on each other:

# roles/api/defaults:
# Переменная запроса
api_auth1_address: "http://example.com:80"
api_auth2_address: "http://example2.com:80"

# roles/auth/defaults:
# Переменная слушатель
auth_bind_port: "20000"

Let's put it in common variables group_vars/all/vars all listeners, and add the name of the group to the name:

# group_vars/all/vars
bbauth_auth_bind_port: "20000"
ghauth_auth_bind_port: "30000"

# group_vars/bbauth/vars
auth_bind_port: "{{ bbauth_auth_bind_port }}"

# group_vars/ghauth/vars
auth_bind_port: "{{ ghauth_auth_bind_port }}"

# group_vars/myapi/vars
api_auth1_address: "http://{{ bbauth_auth_service_name }}:{{ bbauth_auth_bind_port }}"
api_auth2_address: "http://{{ ghauth_auth_service_name }}:{{ ghauth_auth_bind_port }}"

Now, by changing the value of the connector, we will be sure that the request will go to the same port.

code style

  • Since roles and groups are different system objects, they need to have different names so that the link variables will accurately show that they belong to a specific server group, and not to a role in the system.

Environment files

Roles can use files that differ from environment to environment.

SSL certificates are an example of such files. Store them as text
in a variable is not very convenient. But it is convenient to store the path to them inside a variable.

For example, we use the variable api_ssl_key_file: "/path/to/file".

Since it is obvious that the key certificate will change from environment to environment, this is an environment-dependent variable, which means it should be located in the file
group_vars/myapi/vars inventory of variables, and contain the value 'for example'.

The most convenient way in this case is to put the key file in the playbook repository along the path
files/prod/certs/myapi.key, then the value of the variable will be:
api_ssl_key_file: "prod/certs/myapi.key". The convenience lies in the fact that the people responsible for deploying the system on a particular stand also have their own dedicated place in the repository to store their files. At the same time, it remains possible to specify the absolute path to the certificate on the server, in case the certificates are supplied by another system.

Multiple stands in one environment

Often there is a need to deploy several almost identical stands in the same environment with minimal differences. In this case, we divide environment-dependent variables into those that do not change within this environment and those that do. And we take out the latter directly into the inventory files themselves. After this manipulation, it becomes possible to create another inventory directly in the environment directory.

It will reuse the group_vars inventory and also be able to redefine some variables directly for itself.

The final directory structure for the deployment project:

mydeploy                        # Каталог деплоя
├── deploy.yml                  # Плейбук деплоя
├── files                       # Каталог для файлов деплоя
│   ├── prod                    # Католог для средозависимых файлов стенда prod
│   │   └── certs               # 
│   │       └── myapi.key       #
│   └── test1                   # Каталог для средозависимых файлов стенда test1
├── group_vars                  # Каталог переменных плейбука
│   ├── all.yml                 # Файл для переменных связи всей системы
│   ├── myapi.yml               # Файл переменных свойств группы myapi
│   ├── bbauth.yml              # 
│   └── ghauth.yml              #
└── inventories                 #
    ├── prod                    # Каталог окружения prod
    │   ├── group_vars          # Каталог для переменных инвентори
    │   │   ├── myapi           #
    │   │   │   ├── vars.yml    # Средозависимые переменные группы myapi
    │   │   │   └── vault.yml   # Секреты (всегда средозависимы)
    │   │   ├── bbauth          # 
    │   │   │   ├── vars.yml    #
    │   │   │   └── vault.yml   #
    │   │   └── ghauth          #
    │   │       ├── vars.yml    #
    │   │       └── vault.yml   #
    │   └── prod.ini            # Инвентори стенда prod
    └── test                    # Каталог окружения test
        ├── group_vars          #
        │   ├── myapi           #
        │   │   ├── vars.yml    #
        │   │   └── vault.yml   #
        │   ├── bbauth          #
        │   │   ├── vars.yml    #
        │   │   └── vault.yml   #
        │   └── ghauth          #
        │       ├── vars.yml    #
        │       └── vault.yml   #
        ├── test1.ini           # Инвентори стенда test1 в среде test
        └── test2.ini           # Инвентори стенда test2 в среде test

Summing up

After organizing the variables in accordance with the article: each file with variables is responsible for a specific task. And since the file has certain tasks, it became possible to assign a person responsible for the correctness of each file. For example, the developer of the system deployment becomes responsible for the correct filling of the playbook variables, while the administrator, whose stand is described in the inventory, is directly responsible for filling in the inventory of variables.

Roles became a self-contained development unit with their own interface, allowing the role developer to develop features rather than tailoring the role to fit the system. This issue was especially true for common roles for all systems in a campaign.

System administrators no longer need to understand deployment code. All that is required of them for a successful deployment is to fill in the files of environment variables.

Literature

  1. Documentation

Author

Kalyuzhny Denis Alexandrovich

Source: habr.com

Add a comment