dinsdag 27 december 2016

An Ansible role - Tomcat

In my previous post we set out with Ansible. This time I'm going to build on that and take a look at the concept of roles. I will create a role for installing a Tomcat 7 application server.

http://www.deviantart.com/art/The-Black-Tomcat-159041026
Now, to keep the complexity limited (this also gives me topics for future posts) I am going to make a couple of assumptions :
  • Tomcat depends on Java ... I'm going to assume Java is present and in a known location (the same for all the target hosts).
  • The target hosts have access to the internet (this allows them to download the Tomcat archive).
  • The target hosts use upstart.
I understand that because of these assumptions my example may not reflect a real-life situation. I want to keep my posts limited in size though. Rest assured that these difficulties will be addressed in later posts.

A first new concept is that of groups. Those are defined in the Ansible hosts file.
[ansible@ansibleworkstation ~]$ vi /etc/ansible/hosts
ansibleworkstation

[tomcat-servers]
athena


Quite simple, a group ... groups together hosts of the same kind. So here we have a group tomcat-servers. Note that a host can be in multiple groups.

Similar to variables for a host, there are variables for a group.
[ansible@ansibleworkstation ~]$ mkdir /etc/ansible/group_vars
[
ansible@ansibleworkstation ~]$ mkdir /etc/ansible/group_vars/tomcat-servers
[
ansible@ansibleworkstation ~]$ vi /etc/ansible/group_vars/tomcat-servers/vars
tomcat7_http_port: 8080
tomcat7_version: 7.0.73
tomcat7_admin_username: "{{ vault_tomcat7_admin_username }}"
tomcat7_admin_password: "{{ vault_tomcat7_admin_password }}"
[
ansible@ansibleworkstation ~]$ vi /etc/ansible/group_vars/tomcat-servers/vault
vault_tomcat7_admin_username: admin
vault_tomcat7_admin_password: adminsecret
[
ansible@ansibleworkstation ~]$ ansible-vault encrypt /etc/ansible/group_vars/tomcat-servers/vault

Remember that we don't want multiple vault passwords, so use the same one you used for the host variables. I think it's quite clear what the variables mean, so lets move straight on to the playbook.
[ansible@ansibleworkstation ~]$ vi /etc/ansible/playbooks/tomcat7.yml
---
- hosts: tomcat-servers
  remote_user: root
  become: yes
  become_method: sudo
  roles:
    - tomcat7


No rocket science there either, this playbook will execute the role tomcat7 on the hosts in the tomcat-servers group. Before we can run the playbook however we have to define the role. 
[ansible@ansibleworkstation ~]$ mkdir /etc/ansible/roles
[
ansible@ansibleworkstation ~]$ mkdir /etc/ansible/roles/tomcat7
[
ansible@ansibleworkstation ~]$ mkdir /etc/ansible/roles/tomcat7/tasks
[
ansible@ansibleworkstation ~]$ vi /etc/ansible/roles/tomcat7/tasks/main.yml
---
# http://docs.ansible.com/ansible/group_module.html
- name: add group "tomcat"
  group:
    name: tomcat

# http://docs.ansible.com/ansible/user_module.html
- name: add user "tomcat"
  user:
    name: tomcat
    group: tomcat
    home: /usr/share/tomcat
    createhome: no

# http://docs.ansible.com/ansible/unarchive_module.html
- name: extract archive
  unarchive:
    remote_src: yes
    src: "http://archive.apache.org/dist/tomcat/tomcat-7/v{{ tomcat7_version }}/bin/apache-tomcat-{{ tomcat7_version }}.tar.gz"
    dest: /opt
    creates: "/opt/apache-tomcat-{{ tomcat7_version }}"

# http://docs.ansible.com/ansible/file_module.html
- name: symlink installation directory
  file:
    src: "/opt/apache-tomcat-{{ tomcat7_version }}"
    path: /usr/share/tomcat
    owner: tomcat
    group: tomcat
    state: link

# http://docs.ansible.com/ansible/file_module.html
- name: change ownership of the installation
  file:
    path: "/opt/apache-tomcat-{{ tomcat7_version }}"
    owner: tomcat
    group: tomcat
    recurse: yes
    state: directory

# http://docs.ansible.com/ansible/template_module.html
- name: configure server
  template:
    src: server.xml
    dest: "/opt/apache-tomcat-{{ tomcat7_version }}/conf"
    mode: 0600
    owner: tomcat
    group: tomcat

# http://docs.ansible.com/ansible/template_module.html
- name: configure users
  template:
    src: tomcat-users.xml
    dest: "/opt/apache-tomcat-{{ tomcat7_version }}/conf"
    mode: 0600
    owner: tomcat
    group: tomcat

# http://docs.ansible.com/ansible/template_module.html
- name: upstart script
  template:
    src: tomcat7.conf
    dest: "/etc/init"
    mode: 0644
    owner: root
    group: root
  when: ansible_service_mgr == "upstart"

# http://docs.ansible.com/ansible/service_module.html
- name: enable and start service
  service:
    name: tomcat7
    state: started
    enabled: yes


A role has tasks. Note that added a reference to the documentation for each type of task mentioned. What are the tasks ?
  • Create a group tomcat
  • Create a user tomcat
  • Download and extract the Tomcat archive (which one depends on the version variable) into /opt
  • Create a symbolic link /usr/share/tomcat to the installation.
  • Set the ownership of the installation.
  • Update server.xml based on the variables and a template.
  • Update tomcat-users.xml based on the variables and a template.
  • Install the upstart script.
  • Start Tomcat. 
If you study the tasks in detail you'll see that only existing Ansible modules are used (you can of course write your own) and that none of the tasks is complex in/by itself. There is actually only one fancy thing in there and that's the use of the when-clause to determine whether a target host has upstart or not.

Three templates are used in the tasks (server.xml, tomcat-users.xml, tomcat7.conf). Templates are files that are modified based on available variables and facts. Note that I only show the relevant bits of the files below.
[ansible@ansibleworkstation ~]$ mkdir /etc/ansible/roles/tomcat7/templates
[
ansible@ansibleworkstation ~]$ vi /etc/ansible/roles/tomcat7/templates/tomcat-users.xml
<?xml version='1.0' encoding='utf-8'?>
...
<!-- {{ ansible_managed }} -->
...
<tomcat-users>
  <role rolename="manager-gui"/>
  <user username="{{ tomcat7_admin_username }}" password="{{ tomcat7_admin_password }}" roles="manager-gui" />
</tomcat-users>


[ansible@ansibleworkstation ~]$ vi /etc/ansible/roles/tomcat7/templates/server.xml
<?xml version='1.0' encoding='utf-8'?>

<!-- {{ ansible_managed }} -->
...
  <Service name="Catalina">
...
    <Connector port="{{ tomcat7_http_port }}" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443" />
...

[
ansible@ansibleworkstation ~]$ vi /etc/ansible/roles/tomcat7/templates/tomcat7.conf
# {{ ansible_managed }}
description "Tomcat {{ tomcat7_version }} Server"

start on runlevel [2345]
stop on runlevel [!2345]
respawn
respawn limit 10 5

setuid tomcat
setgid tomcat

env JAVA_HOME=/usr/lib/jvm/java-8-oracle/jre
env CATALINA_HOME=/opt/apache-tomcat-{{ tomcat7_version }}
env JAVA_OPTS="-Djava.awt.headless=true -Djava.security.egd=file:/dev/./urandom"
env CATALINA_OPTS="-Xms512M -Xmx1024M -server -XX:+UseParallelGC"

exec $CATALINA_HOME/bin/catalina.sh run

# cleanup temp directory after stop
post-stop script
  rm -rf $CATALINA_HOME/temp/*
end script


And that concludes our role definition. Now we can run the playbook.
[ansible@ansibleworkstation ~]$ ansible-playbook /etc/ansible/playbooks/tomcat7.yml --ask-vault-pass

Cleanup up is not automated at the moment, so if you want to experiment a bit, here are the instructions to clean up on a target host.
ansible@athena:~$ sudo service tomcat7 stop
tomcat7 stop/waiting
 
ansible@athena:~$ sudo rm -rf /etc/init/tomcat7.conf
ansible@athena:~$ sudo initctl reload-configuration
ansible@athena:~$ sudo rm -rf /usr/share/tomcat 
ansible@athena:~$ sudo rm -rf /opt/apache-tomcat-7.0.72/
ansible@athena:~$ sudo userdel tomcat
ansible@athena:~$ sudo groupdel tomcat
groupdel: group 'tomcat' does not exist


As you can see, roles are very powerful and they can be customized with your own variables or with system facts. Enjoy !