Chef:Powerful Infrastructure Automation
上QQ阅读APP看书,第一时间看更新

Attributes

Attributes are Chef's way of storing configuration data and can be thought of as a large, but disjointed, hash structure. Chef pulls data from various locations and combines that data in a specific order to produce the final hash of attributes. This data is computed when a client requests its run list from the server (such as when you execute chef-client on a node). This mechanism allows you to describe data with a higher level of specificity at each step of the process, decreasing in scope going from the cookbook attributes files down to node-specific configuration data.

For example, imagine you are deploying PostgreSQL onto the hosts in your infrastructure. With PostgreSQL, there are a very large number of configuration options that can be tuned, ranging from open ports and number of concurrent connections down to memory used for key caches and other fine-grained configuration options. The cookbook's attributes files should provide enough configurations for PostgreSQL to work without making any modification to a host and without other things being deployed; also, they would most likely contain a pretty vanilla set of configuration values, which at a high level might look like the following:

  • Install Version 9.3 from the source code
  • Listen on Port 5432 on IP 0.0.0.0/0
  • Store data in /var/lib/postgresql
  • Create and use a pgsql user

Attribute data has not only a number of sources that it can be pulled from but also a set of priorities: default, normal, and override (in increasing order). Within each level, data is pulled from the cookbook attributes files and then from the environment, role, and node configuration data stored in the Chef server (in that order). Combined, this provides a comprehensive mechanism to define and customize the behavior of your recipes as they are applied to the nodes.

Now, as you can imagine, this is fine for a number of installations where the server has all of the space allocated on the root mount point, or doesn't have security restrictions about which IP addresses should be listened on, and so on. It would be nice to be able to say that in production, we want to use Version 9.3, but in a test environment, we want to install Version 9.4 in order to perform some tests that we don't want to run in production. We may also want to specify that in production, our hosts are EC2 instances with a customized EBS RAID for our PostgreSQL data and so the data should be stored in /vol/ebs00/postgresql. Using this multilayered approach for configuration data, this is entirely possible.

Attribute files contain Ruby code that stores the configuration data. In this case, to achieve our described default behavior, we could have a file, attributes/default.rb, that contains the following text:

default['postgresql']['port'] = "5432"
default['postgresql']['listen_address'] = "*"
default['postgresql']['data_dir'] = "/var/lib/postgresql"
default['postgresql']['install_method'] = "source"
default['postgresql']['version'] = "9.3"

The hash that this describes will look like the following JSON dictionary:

'node' : {
  'postgresql': {
    'port': '5432',
    'listen_address': '*',
    'data_dir': '/var/lib/postgresql',
    'install_method': 'source',
    'version': '9.3'
   }
}

Now, as described, we want to override the version in our staging environment to install Version 9.4; this means that in our staging environment, the configuration (exactly how to make this change will be discussed later) will need to have the following information:

 'node' : {
  'postgresql': {
    'version': '9.4',
  }
}

When the Chef client runs on a node in the staging environment, the Chef server knows that the node is in the staging environment and will take the stage configuration above and overlay it on top of the defaults specified in the cookbook. As a result, the final configuration dictionary will look like the following:

'node' : {
  'postgresql': {
    'port': '5432',
    'listen_address': '*',
    'data_dir': '/var/lib/postgresql',
    'install_method': 'source',
    'version': '9.4'
   }
}

Notice that the version has been changed, but everything else remains the same. In this way, we can build very specific configurations for our hosts that pull information from a variety of places.

It is important to note that because these are interpreted Ruby scripts, their contents can range from simple attribute-setting statements to complex logic used to determine an appropriate set of default attributes. However, it's worth remembering that the more complicated your configuration is, the harder it may be to understand.

Multiple attribute files

Chef loads attribute files in alphabetical order and cookbooks typically contain only one attribute file named default.rb. In some cases, it makes sense to separate some of the attributes into separate files, particularly when there are a lot of them. As an example, the community-maintained MySQL cookbook has two attribute files: server.rb for the server attributes and client.rb with client-specific attributes. Each file contains anywhere between 50 and 150 lines of Ruby code, so it makes sense to keep them separate and focused.

Supporting multiple platforms

There are times when a simple attributes file doesn't make sense, so being able to dynamically define the defaults is very useful. Consider a multiplatform cookbook that needs to know which group the root user is in. The name of the group will vary according to the operating system of the end host. If you are provisioning a FreeBSD or OpenBSD host, then the group for the root will be wheel, but on an Ubuntu machine, the group is named admin. The attributes file can use plain old Ruby code or optional Chef-provided convenience methods such as value_for_platform, which is a glorified but compact switch statement:

default[:users]['root'][:group] = value_for_platform(
  "openbsd"   => { "default" => "wheel" },
  "freebsd"   => { "default" => "wheel" },
  "ubuntu"    => { "default" => "admin" },
  "default"   => "root"
)

Loading external attributes

Sometimes it is useful to load attributes from another cookbook; if your cookbook is tightly coupled to another cookbook or provides some extended functionality, it may make sense to use them. This can be achieved in the attributes file by using the include_attribute method (again, this is a Chef-specific convenience method).

Let's consider that you want to know the port that Apache is configured to use. You could use the port key from the apache configuration section, but it is not guaranteed that it exists (it may not have been defined or the recipe that contains it may not have been loaded yet). To address this, the following code would load the settings from attributes/default.rb inside of the apache cookbook:

include_attribute "apache"
default['mywebapp']['port'] = node['apache']['port']

If you need to load an attributes file other than default.rb, say client.rb, inside the postgresql cookbook, you can specify it in the following manner:

include_attribute "postgresql::client"

Make sure that any cookbooks you rely on are listed as a dependency in your cookbook's metadata. Without this, the Chef server will have no way of knowing that your recipes or configuration data depend on that cookbook, and so your configuration may fail as a result of this.

Using attributes

Once you have defined your attributes, they are accessible in our recipes using the node hash. Chef will compute the attributes in the order discussed and produce one large hash, which you will have access to.

Tip

Chef uses a special type of hash, called a Mash. Mashes are hashes with what is known as indifferent access—string keys and symbol keys are treated as the same, so node[:key] is the same as node["key"]).

If we had the PostgreSQL attributes and user attributes as specified previously, without any overrides, then the resulting configuration will look like the following:

'node' : {
  'postgresql': {
    'port': '5432',
    'listen_address': '*',
    'data_dir': '/var/lib/postgresql',
    'install_method': 'source',
    'version': '9.3'
   }, 
   'users' : {
     'root' : { 'group' => 'wheel' },
   }

}

This data could then be accessed anywhere in our recipes or templates through variables such as node[:postgresql][port] or node[:users][:root][:group]. Remember that the final version of the node's configuration data is determined at the time the client makes the request for its configuration. This means that Chef generates a snapshot of the current state of the system, collapsed according to its rules of precedence, for that node and passes it to the host to perform its operations.