📜 ⬆️ ⬇️

Automatic configuration: the practice of using CFEngine in the real world

CFEngine


Let's continue the story about CFEngine begun by the user alex_www in two previous articles. This discussion will focus on the practice of using CFEngine and some of the nuances of setting it up in the real world. To reduce the amount of text, I assume that you own the basic concepts from the world of CFEngine, maybe even tried to use it somewhere. As a primer, I can advise Diego Zamboni’s book " Learning CFEngine 3 ", which is small, very understandable and read in one breath.

The article provides examples for setting up from scratch on Debian GNU / Linux using Git . If you want to supplement the article with examples for your favorite distros and VCS, then send private messages or comment in the comments. If possible, I will add them to the main text indicating the authorship.

Installation


The easiest way to install CFEngine3 is through the official repository. To do this, you first need to add a key with which the packages are signed:

cd /tmp wget http://cfengine.com/pub/gpg.key cat gpg.key #      apt-key add gpg.key rm gpg.key #  ,    

')
... and then add the repository and put the CFE:

 echo "deb http://cfengine.com/pub/apt community main" > /etc/apt/sources.list.d/cfengine-community.list chmod 644 /etc/apt/sources.list.d/cfengine-community.list #   umask 0700 apt-get update apt-get install cfengine-community 


Initialization


To initialize CFE, you need to perform (for example, manually) the first run of the cf-agent. If you have to frequently launch new servers, the installation of CFE and bootstrap is best done from preseed or from a script that starts once when the system is first started; A good example of such a script is the script that creates server keys for OpenSSH.

 /var/cfengine/bin/cf-agent -IC --bootstrap <policy_hub_address> 


Instead of <policy_hub_address> you need to substitute the appropriate IP address or domain name of your policy hub, and if you initialize the policy hub itself, then you need to specify the IP on which it will serve clients later. In the case of a domain name, it will be resolved to an IP address, which will be used in the future, so that problems with the possible unavailability of DNS servers will not interfere.

The -I enables the creation of a short report on the execution, and -C - coloring the output in the console. Both are optional, but I use them in interactive sessions for my own convenience. Another useful startup option is -v , verbose mode. It takes precedence over -I and gives you very detailed information on the progress of the promises. Nice helps when debugging.

Primary setup


After completing the initial policy hub, before connecting to the first client, you need to configure some little things. The fact is that the default settings in the def.cf file (hereinafter, for all relative paths, the root is /var/cfengine/masterfiles , unless otherwise specified) are well suited for “on the knee” experiments or for demonstration of capabilities, but for Sell ​​these options are not enough. Below, I will write about the organization of the process of developing promises and rolling them out in the product, but for now it is best to make a local copy of /var/cfengine/masterfiles and work with it.

First of all, let's specify our full domain name. Despite the efforts of the development team to make a good system for auto-detecting the current system parameters, it is not ideal, so wherever possible and appropriately it is best not to rely on automation and specify the data manually. Let's start with the bundle common def , and specify the domain , mailto , mailfrom and smtpserver :

 'domain' string => 'example.org'; 'mailto' string => 'sysadmin-queue@${def.domain}'; #    CFE        'mailfrom' string => 'root@cfe-policy-server.${def.domain}'; 'smtpserver' string => 'internal-mail-collector.${def.domain}'; 


Security


CFE can ensure the security of connections and client authentication, however it does not have any exceptionally developed means for this, as there are enough available eyes to complete the task. Anyway, in communications with clients, a system with trusted keys is used, similar to OpenSSH, and lists of trusted IP networks and domains. By default, connections are allowed for all hosts in the policy hub domain and for / 16 of its primary IP. All keys received from successfully established connections are considered trusted. Such an approach in working conditions in a trusted network can greatly facilitate the deployment from scratch and is very convenient at the R & D stage, but it is extremely unsafe in real life. Given the ephemeral nature of IP addresses and the geographical distribution of monitored machines, I prefer to use the following approach: CFEngine ( cf-serverd , to be more precise) accepts connections from any IP address and only trusts those keys that it knows in advance (that is, are located in /var/cfengine/ppkeys ):

  'acl' slist => { '0.0.0.0/0', }; comment => 'Connections are allowed from any IP', handle => 'common_def_vars_acl'; 'trustkeysfrom' slist => { # NEVER ADD ANYTHING HERE. DON'T TRUST STRANGERS! }, comment => 'Only keys in /var/cfengine/ppkeys are trusted', handle => 'common_def_vars_truskeysfrom'; 


In the case when it is necessary to restrict access to IP addresses, I prefer to use a firewall as a tool more suitable for solving the problem.

To add a client key to trusted files, copy the contents of the /var/cfengine/ppkeys/localhost.pub file (the usual RSA key in Base64) to the policy hub and run cf-key -t /path/to/client_key.pub . The cf-key program will add it to /var/cfengine/ppkeys with the correct name and permissions.

Standard library


Automation of configuration control, which makes it easy to make large-scale changes, also leads to large-scale errors with the same ease. Therefore, a reasonable number of "seat belts" and "big red buttons." One of these “belts” is the insertion of the standard library into the VCS along with your code. When installing a package with a new version, the contents of the /var/cfengine/masterfiles and /var/cfengine/inputs not updated, as a result, it will be impossible to guarantee the consistency of the configuration. Therefore, one of the important stages of updating is merging the changes of the standard library with your copy and it is here that all the help your VCS can offer you is useful, as well as the ability to use the diff and patch utilities.

package_latests


One of the mechanisms that I often use to guarantee updates of some packages is the package_latest bundle from the standard library. Unfortunately, there is a bug because of which the bundle in Debian does not work. Fix is ​​quite trivial. In the file lib/3.6/packages.cf you need to find the bundle code packages_latest and bring it to this form (you can use a patch from the bug report):

 debian:: "$(package)" package_policy => "addupdate", package_version => "999999999:9999999999", package_method => apt_get_permissive; 


Bug 6870


Another annoying bug that I discovered in the sale is bug 6870 . The essence of the problem is that CFEngine establishes some classes based on PTR records of IP addresses on interfaces. As you can guess, this behavior of the system is very, very unsafe, and it contradicts the CFEngine postulate about the unacceptability of external knowledge. However, in his book, Diego Zamboni teaches how to determine the execution host by the class of the host1_example_org type, and fixing this bug can break too many systems currently working. Therefore, until the developers have provided a more reliable method, we will create it ourselves. Here is the code that you can add to your version of the standard library in the file lib/3.6/bug_6870.cf :

 bundle common bug_6870_workaround { classes: 'bug6870_workaround_${sys.host}' expression => 'any'; } 


Then you need to add the file name to ${stdlib_common.inputs} by analogy with the files already listed there, and after that use the bug6870_workaround_host1_example_org class without fear of unexpected intersections with the PTR records of other IPs.

Life cycle


Now, when everything is ready for creativity, I will tell you a little about the organization of the process of developing promisses and a bit about more mundane, everyday things.

Instruments


First of all, promises should be written in something. For this, your favorite text editor will most likely fit. I prefer Vim and use plugins written by Neil Watson (Niel Watson), and my colleague Valera Ostroverhov (Val Astraverkhau) created a very good plug-in to support CFEngine in Sublime Text 2 and 3. Emacs users will be interested in Ted Zlatanov's lecture (Ted Zlatanov) using Emacs as a CFEngine IDE.

You will also need a good version control system. Git suits me all, but I'm sure any modern VCS will do. The requirements here are the same as for the development of ordinary software, so take what you are comfortable with.

File Organization and Entry Point


I will say right away that the method proposed by me is not the only correct one. The principle of TIMTOWTDI, familiar to Perl programmers, is applicable here; and nowhere else, controversy is appropriate. Here is an example directory structure, relative to the project root:

 /bin /masterfiles /masterfiles/cfe_internal /masterfiles/cfe_internal/ha /masterfiles/controls /masterfiles/controls/3.4 /masterfiles/inventory /masterfiles/example_org /masterfiles/lib /masterfiles/lib/3.5 /masterfiles/lib/3.6 /masterfiles/services /masterfiles/services/autorun /masterfiles/sketches /masterfiles/sketches/meta /masterfiles/templates /masterfiles/update /static /static/bird-lg /static/firewall-configs /static/ssh-keys /templates 


In the directory /masterfiles/example_org is the code that we write. The rest of the subdirectories in /masterfiles are parts of the standard delivery that I try not to change unless absolutely necessary. All non-standard templates are in /templates , and in /static , as the name implies, “static” information is stored - public SSH-keys, firewall settings, user settings, configuration files, etc., which does not change from host to host. There are a couple of service scripts in the /bin directory, including the “big red button” - a script that puts all the necessary files to where CFEngine can distribute them to customers.

The entry point is located in /masterfiles/example_org/main.cf , which contains two promises: bundle common example_org , which lists the files used and the servers are classified, and the bundle agent example_org_main , where, depending on the class, control is transferred to the necessary bundle, which describes exactly how Servers of this class must be configured.

To specify the entry point in the promises.cf file, you need to make the following changes:

 body common control { bundlesequence => { # [...] @{example_org.bundles}, }; inputs => { # [...] 'example_org/main.cf', @{example_org.inputs}, }; } 


example_org/main.cf looks like this:

 bundle common example_org { vars: 'inputs' slist => { 'example_org/add_default_users.cf', 'example_org/basic_packages.cf', 'example_org/configure_dns.cf', 'example_org/configure_firewall.cf', 'example_org/configure_ftp.cf', 'example_org/configure_ssh.cf', 'example_org/cve_2015_0235.cf', 'example_org/lib.cf', }; 'bundles' slist => { 'example_org', 'example_org_main', }; classes: 'ftp_server' or => {classmatch('BUG6870_ftp.*')}; 'dns_server' expression => classmatch('BUG6870_dns.*'); reports: verbose_mode:: '${this.bundle}: defining inputs="${inputs}"'; '${this.bundle}: defining bundles="${bundles}"'; ftp_server:: 'This host assumes FTP server role'; dns_server:: 'This host assumes DNS server role'; } bundle agent example_org_main { methods: any:: 'example_org_update_motd' usebundle => 'update_motd'; 'example_org_basic_packages' usebundle => 'basic_packages'; 'example_org_add_default_users' usebundle => 'add_default_users'; 'example_org_configure_firewall' usebundle => 'configure_firewall'; 'example_org_configure_ssh' usebundle => 'configure_ssh'; 'example_org_cve_2015_0235' usebundle => 'cve_2015_0235'; any.Min30_35:: 'heartbeat' usebundle => 'heartbeat'; # FTP servers configuration ftp_server:: 'example_org_configure_ftp' usebundle => 'configure_ftp'; # DNS servers configuration dns_server:: 'example_org_configure_dns' usebundle => 'configure_dns'; } 


This example was made up of several real projects for demonstration, in reality, of course, everything is a little more complicated and more. The purpose of this approach is to minimize the number of changes in the standard library, so that later it would be easier to maintain changes in it.

Debugging and Testing


Since the price of a mistake is high, before clicking on the “big red button”, it’s worth testing your promises. For tests, I have a small polygon: several virtual machines that I run on my work computer. On them, I check the correctness of the promises and do experiments.

In the development of promises, I adhere to the style of school debugging through printf, that is, I use report promises very widely. Another indispensable tool is the complete cf-promises utility. In addition to the formal validation of the syntax, it can show all classes available at runtime (the --show-classes parameter) and variables with their contents (the --show-vars ). And of course, running cf-agent in verbose mode ( --verbose option).

Updates


You can update the CFEngine version on all controlled machines in two ways: either through the package manager, or using the mechanisms of CFEngine itself. I prefer the package manager, for which I have a special promise, which I connect only for the duration of updates, its essence comes down to calling package_latest . In my opinion, this approach best suits the concepts of CFE.

Big Red Button


Rolling out the food is always a little exciting moment, even if it happens many times a day, and I do not blame people who have some kind of ritual for that. In the case of massive configuration changes, it can determine the meaning of life for the next few days if something goes wrong. Therefore, no automation, no hooks for Git. Only manual mode, as a pledge of confidence that everything is done correctly, tested and ready for sale. I have a deploy.sh script with 0600 permissions as a button, so that it can’t be run at random. Using my hands in the bash bin/deploy.sh console bash bin/deploy.sh is my ritual and the last opportunity to cancel the launch. The script itself is quite trivial: using rsync it synchronizes masterfiles , static and templates with the contents of /var/cfengine/{masterfiles,static,templates} and runs two commands: cf-agent -KIC -f update.cf and cf-agent -KIC -f promises.cf . So I can be sure that at a minimum a policy hub can perform promises and distribute them to all customers.

Conclusion


This is not all the subtleties and wisdom, but this is quite enough to start the introduction of CFEngine in themselves. Such interesting topics as " Design Center ", reports, internal structure, various usage scenarios and much more remained outside the scope of the article. If CFEngine is of some interest to the habrasoobshchestva, I will gladly tell you more about it, but for now, if you have any pressing questions right now, feel free to ask them in the comments. My colleague on lastops cagliostro and I will try to answer them.

Source: https://habr.com/ru/post/249249/


All Articles