For the Linux server group set in Ansible, I tried to modify (?) Ansible and serverspec in order to solve the following problems that I had when testing with serverspec.
serverspec-init
, it is troublesome to prepare the test code for each directory with the name of the server to be tested.Please refer to it as an introductory / beginner's edition when using Ansible and serverspec in cooperation.
Implement so that you can:
Ansible Control Node
Ansible Managed Node
$ tree -aF /autotools
/autotools
|-- .ssh/
| `-- aws_key.pem #Managed Node SSH private key
|-- ansible/
| |-- ansible.cfg
| |-- group_vars/ #Variable directory for groups
| |-- host_vars/ #Variable directory for host
| |-- inventory/ #Inventory placement directory for Ansible
| `-- centos.yml # Playbook
`-- serverspec/
|-- .rspec
|-- Rakefile
|-- spec/
| |-- base/ #Test coat placement directory for base role
| | `-- sample_spec.rb #Test code
| `-- spec_helper.rb
`-- spec_hosts/ #Variable placement directory for serverspec
Ansible
First, let's decide project_name
with an English character string for the purpose of managing this server group collectively.
Here, as an example, use project_name
as ʻanken`.
ansible.cfg
Here, specify ʻansible.cfg to be used in the environment variable ʻANSIBLE_CONFIG
.
I'm using the same hostname in my code development, so I've listed the ssh arguments here.
$ export ANSIBLE_CONFIG=/autotools/ansible/ansible.cfg
/autotools/ansible/ansible.cfg
[defaults]
[ssh_connection]
ssh_args = -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no
[privilege_escalation]
become = true
Place the SSH private key used to SSH into the Ansible Managed Node in /autotools/.ssh
.
The key path is listed in the inventory file.
Place the inventory file ʻanken.iniunder
/ autotools / ansible / inventory /`.
Basically, it is OK if you write according to Ansible rules, but since it is used in the mechanism for linking with serverspec, please set the following three indispensable.
project_name
with[all: vars]
, ʻansible_password
, and ʻansible_ssh_private_key_file`, respectively. Set either a password or an SSH key. (The password and SSH key written here are also used in serverspec through the variable file described later.)/autotools/ansible/inventory/anken.ini
[anken]
prod_foobar1 ansible_host=xx.xx.xx.xx
dev_foobar1 ansible_host=yy.yy.yy.yy
[anken:vars]
ansible_user=centos
ansible_ssh_private_key_file=~/.ssh/aws_key.pem
[all:vars]
project_name=anken
Ansible is executed for the IP or name of ʻansible_host`.
ʻInventory_hostname (where
prod_foobar1
dev_foobar2` is specified in the example) does not have to match the actual hostname of the node.
Playbook
The playbook example used in this implementation example is as follows.
name: Configure for serverspec at localhost
outputs the variable file used by serverspec.
centos.yml
---
- name: Playbook for centos7 managed node
hosts: all
gather_facts: true
tasks:
- name: Create group
group:
name: "{{ item.name }}"
gid: "{{ item.gid }}"
loop: "{{ group }}"
tags: group
- name: Create User
user:
name: "{{ item.name }}"
uid: "{{ item.uid }}"
group: "{{ item.group }}"
groups: "{{ item.groups }}"
home: "{{ item.home }}"
shell: "{{ item.shell }}"
loop: "{{ user }}"
tags: user
- name: System service
systemd:
name: "{{ item.name }}"
enabled: "{{ item.enabled }}"
state: "{{ item.state }}"
loop: "{{ service }}"
tags: service
- name: Configure for serverspec at localhost
hosts: localhost
connection: local
gather_facts: false
tasks:
- name: Dump hostvars for serverspec
copy:
content: "{{ hostvars | to_nice_yaml }}"
dest: "../serverspec/spec_hosts/{{ project_name }}.yml"
tags: serverspec
Variables common to projects are placed in /autotools/ansible/group_vars/#{project_name}.yml
. If you want to specify a different variable only for a specific inventory_hostname, place it in /autotools/ansible/host_vars/#{inventory_name}.yml
.
In the variable file, specify the role to use when testing with serverspec with serverspec_role
.
/autotools/ansible/group_vars/anken.yml
serverspec_role:
- base
group:
- name: unyo
gid: 1101
- name: infra
gid: 1102
- name: app
gid: 1103
user:
- name: user1
uid: 2001
group: customer
groups: [ unyo, infra]
home: /home/user1
shell: /bin/bash
- name: user2
uid: 2002
group: customer
groups: [ app ]
home: /home/user2
shell: /bin/bash
- name: user3
uid: 2003
group: customer
groups: [ app, infra ]
home: /home/user3
shell: /bin/bash
service:
- name: chronyd.service
enabled: false
state: stopped
- name: rsyncd.service
enabled: true
state: started
Here, I will limit it to the prod_foobar
node and overwrite some variables.
/autotools/ansible/group_vars/prod_foobar.yml
group:
- name: unyo
gid: 2101
- name: infra
gid: 2102
- name: app
gid: 2103
After arranging the necessary files, it should look like this.
$ tree /autotools/ansible -aF
/autotools/ansible
|-- ansible.cfg
|-- centos.yml
|-- group_vars/
| `-- anken.yml
|-- host_vars/
| `-- prod_foobar1
`-- inventory/
`-- anken.ini
Run the centos.yml playbook with an inventory file as shown below.
$ cd /autotools/ansible
$ ansible -i ./inventory/anken centos.yml
serverspec
After running Ansible, the directory / file structure on the serverspec side should look like this.
# tree /autotools/serverspec -aF
/autotools/serverspec
|-- .rspec
|-- Rakefile
|-- spec/
| |-- base/
| | `-- sample_spec.rb
| `-- spec_helper.rb
`-- spec_hosts/
`-- anken.yml #Variable file generated by Ansible
The order is different, but the execution command of serverspec is as follows. This is an image of passing the variable file name (generated by Ansible) used in serverspec to the rake command as an argument.
$ rake spec anken -T
rake spec # Run spec to all hosts
rake spec:dev_foobar1 # Run spec to dev_foobar1
rake spec:prod_foobar1 # Run spec to prod_foobar1
$ rake spec anken
Rakefile
There are some changes from the standard Rakefile
created by serverspec-init
.
/autotools/serverspec/Rakefile
require 'rake'
require 'rspec/core/rake_task'
require 'yaml'
#Read variable file
project_name = ARGV[1]
hosts = YAML.load_file("./spec_hosts/#{project_name}.yml")
desc "Run spec to all hosts"
task :spec => 'spec:all'
namespace :spec do
task :all => hosts.keys.map {|key| 'spec:' + key }
hosts.keys.each do |key|
desc "Run spec to #{key}"
RSpec::Core::RakeTask.new(key.to_sym) do |t|
ENV['INVENTORY_HOST'] = key
ENV['PROJECT_NAME'] = project_name
# serverspec_Under a directory with the same name as role*_spec.Read rb file
t.pattern = 'spec/{' + hosts[key]['serverspec_role'].join(',') + '}/*_spec.rb'
t.fail_on_error = false
end
end
end
#Forged the argument of the rake command as an empty task
ARGV.slice(1,ARGV.size).each{|v| task v.to_sym do; end}
I used it as a reference below. Thank you very much.
Reference: Write ordinary argument-like processing in Rake task https://qiita.com/nao58/items/aa50514d97f05eb8d128
Reference: Official How to use host specific properties https://serverspec.org/advanced_tips.html
spec_helper.rb
This also changes some functions from the initial state of spec_helper.rb
.
/autotools/serverspec/spec/spec_helper.rb
require 'serverspec'
require 'pathname'
require 'net/ssh'
require 'yaml'
#Read variable yml file
key = ENV['INVENTORY_HOST']
project_name = ENV['PROJECT_NAME']
properties = YAML.load_file("./spec_hosts/#{project_name}.yml")
set_property properties["#{key}"]
set :backend, :ssh
set :path, '/sbin:/usr/sbin:$PATH'
#ssh execution part
RSpec.configure do |c|
c.before :all do
#Extract the host, user, password or key used in Ansible from the read variable file
set :host, property['ansible_host']
options = Net::SSH::Config.for(c.host)
options[:user] = property['ansible_user']
if property['ansible_password']
options[:password] = property['ansible_password']
else
options[:keys] = [ property['ansible_ssh_private_key_file'] ]
end
options[:user_known_hosts_file] = '/dev/null'
set :ssh_options, options
end
end
This spec_helper.rb
does not support WinRM because it is set: backend,: ssh
. However, since you can write anything in Ruby, it shouldn't be difficult to support Windows.
This is a sample.
As written in spec_helper.rb
,property ['xxx']
can be used to retrieve a variable from a variable file and reuse it.
/autotools/serverspec/spec/base/
# frozen_string_literal: true
require 'spec_helper'
puts "\nRun serverspec to #{property['inventory_hostname']}"
property['group'].each do |attr|
describe group(attr['name']) do
it { should exist }
it { should have_gid attr['gid'] }
end
end
property['user'].each do |attr|
describe user(attr['name']) do
it { should exist }
it { should have_uid attr['uid'] }
it { should belong_to_group attr['group'] }
end
end
property['service'].each do |attr|
describe service(attr['name']) do
attr['enabled'] ? it { should be_enabled } : it { should_not be_enabled }
attr['state'] == 'started' ? it { should be_running } : it { should_not be_running }
end
end
Again, execute the rake spec command with the variable file name generated by Ansible as an argument as shown below. It is also possible to execute tests for each unit.
$ rake spec anken
$
$ rake spec anken -T #Command to display task list
rake spec # Run spec to all hosts
rake spec:dev_foobar1 # Run spec to dev_foobar1
rake spec:prod_foobar1 # Run spec to prod_foobar1
$
$ rake spec:dev_foobar1 anken
With Ansible and serverspec, we were able to unify variable files and event refiles, which tend to be double-managed. In addition, I was able to manage and execute the test code on a role-by-role basis by referring to the method described in the serverspec formula.
Since serverspec is a pretty Ruby-colored tool, it's hard for people who don't usually touch Ruby, but once you get used to it, it's good that various processes are easy to write.
It is published below. https://github.com/kentarok/autotools