Using Ansible with Docker to Deploy a WordPress Service on Rancher

on Nov 3, 2015

Ansible-Docker-RancherOver the last year I’ve been using Rancher with Ansible, and have found that using the two together can be incredibly useful. If you aren’t familiar with Ansible, it is a powerful configuration management tool which can be used to manage servers remotely without a daemon or agent running on the host.  Instead, it uses SSH to connect with hosts, and applies tasks directly on the machines.  Because of this, as long as you have SSH access to the host, (and Python) running on the host,  you will be able to use Ansible to manage hosts remotely. You can find detailed  documentation for Ansible on the company’s website..

In this post, I will be using Ansible with Docker to automate the build out of a simple wordpress environment on a Rancher deployment.  Specifically, I will include the following steps:

  • Installing Docker on my hosts using Ansible.
  • Setting up a fresh Rancher installation  using Ansible.
  • Registering hosts with Rancher using Ansible.
  • Deploying the Application containers on the Hosts.

Preparing the Playbook

Ansible uses “playbooks’  which are Ansible’s configuration and orchestration language, These playbooks are expressed in YAML format, and describes set of tasks that will run on remote hosts, see this introduction for more information on how to use Ansible playbooks

in our case the playbook will run on 3 servers, one server for the Rancher platform, the second server  for the MySQL database, and the last one for the WordPress application.

The addresses and information about the previous servers are listed in the following Ansible inventory file, the inventory is the file that contains names, addresses, and ports of the remote hosts where the Ansible playbook is going to execute:

inventory file:

[Rancher]
rancher ansible_ssh_port=22 ansible_ssh_host=x.x.x.x

[nodes:children]
application
database

[application]
node1 ansible_ssh_port=22 ansible_ssh_host=y.y.y.y

[database]
node2 ansible_ssh_port=22 ansible_ssh_host=z.z.z.z

 

Note that I used grouping in the inventory to better describe the list of machines used in this deployment, The playbook itself will consists of five plays, which will result in deploying the WordPress application:

  • Play #1 Installing and configuring Docker

The first play will install and configure Docker on all machines, it uses the “docker” role which we will see in the next section.

  • Play #2 Setting up Rancher server

This play will install Rancher server and make sure it is up and running, this play will only run on one server which is considered to be the Rancher server.

  • Play #3 Registering Rancher hosts

This play will run on two machines to register each of them with the Rancher server which should  be up and running from the last play.

  • Play #4 Deploy MySQL Container

This is a simple play to deploy the MySQL container on the database server.

  • play #5 Deploy WordPress App

This play will install the WordPress application on the second machine and link it to the MySQL container.

rancher.yml (the playbook file)

---
# play 1
- name: Installing and configuring Docker 
  hosts: all
  sudo: yes
  roles:
    - { role: docker, tags: ["docker"] }

# play 2
- name: Setting up Rancher Server
  hosts: "rancher"
  sudo: yes
  roles:
    - { role: rancher, tags: ["rancher"] }

# play 3
- name: Register Rancher Hosts
  hosts: "nodes"
  sudo: yes
  roles:
    - { role: rancher_reg, tags: ["rancher_reg"] }

# play 4
- name: Deploy MySQL Container
  hosts: 'database'
  sudo: yes
  roles:
      - { role: mysql_docker, tags: ["mysql_docker"] }

# play 5
- name: Deploy WordPress App
  hosts: "application"
  sudo: yes
  roles:
    - { role: wordpress_docker, tags: ["wordpress_docker"] }

Docker role

This role will install the latest version of Docker on all the servers, the role assumes that you will use Ubuntu 14.04, because some other Ubuntu distros require some dependencies to run docker which is not discussed here, see the Docker documentation for more information on installing Docker on different platforms.

- name: Fail if OS distro is not Ubuntu 14.04
  fail: 
      msg="The role is designed only for Ubuntu 14.04"
  when: "{{ ansible_distribution_version | version_compare('14.04', '!=') }}"

The Docker module in Ansible requires docker-py library to be installed on the remote server, so at first we use python-pip to install docker-py library on all servers before installing the Docker:

- name: Install dependencies
  apt: 
      name={{ item }} 
      update_cache=yes
  with_items: 
      - python-dev
      - python-setuptools

- name: Install pip
  easy_install: 
      name=pip 

- name: Install docker-py
  pip: 
      name=docker-py 
      state=present
      version=1.1.0

The next tasks will import the Docker apt repo and install Docker:

- name: Add docker apt repo
  apt_repository:
      repo='deb https://apt.dockerproject.org/repo ubuntu-{{ ansible_distribution_release }} main'
      state=present

- name: Import the Docker repository key
  apt_key:
      url=https://apt.dockerproject.org/gpg
      state=present
      id=2C52609D

- name: Install Docker package
  apt:
      name=docker-engine
      update_cache=yes

Finally the next three tasks will create a system group for Docker and add any user defined in “docker_users” variable to this group, and it will copy template for Docker configuration then restart Docker.

- name: Create a docker group
  group: 
      name=docker 
      state=present

- name: Add user(s) to docker group
  user: 
      name={{ item }} 
      group=docker 
      state=present
  with_items: docker_users
  when: docker_users is defined

- name: Configure Docker 
  template: 
      src=default_docker.j2 
      dest=/etc/default/docker 
      mode=0644 
      owner=root 
      group=root
  notify: restart docker

The “default_docker.j2” template will check for the variable “docker_opts” which is not defined by default, and if it is defined will add the options defined in the variable to the file:

# Docker Upstart and SysVinit configuration file

# Use DOCKER_OPTS to modify the daemon startup options.
{% if docker_opts is defined %}
DOCKER_OPTS="{{ docker_opts | join(' ')}}"
{% endif %}

Rancher role

The rancher role is really simple, its goal is to pull and run the Rancher’s Docker image from the hub, and then wait for the Rancher server to start and listen for incoming connections:

---
- name: Pull and run the Rancher/server container
  docker:
      name: "{{ rancher_name }}"
      image: rancher/server
      restart_policy: always
      ports: 
        - "{{ rancher_port }}:8080"

- name: Wait for the Rancher server to start
  action: command docker logs {{ rancher_name }}
  register: rancher_logs
  until: rancher_logs.stdout.find("Listening on") != -1
  retries: 30
  delay: 10

- name: Print Rancher's URL
  debug: msg="You can connect to rancher server http://{{ ansible_default_ipv4.address }}:{{ rancher_port }}"

Rancher Registration Role

The rancher_reg role will pull and run the rancher_agent Docker image, first it will use Rancher’s API to return the registration token to run each agent with the right registration url, this token is needed to register hosts in Rancher environment:

---
- name: Install httplib2
  apt:
      name=python-httplib2
      update_cache=yes

- name: Get the default project id
   action: uri
       method=GET
       status_code=200
       url="http://{{ rancher_server }}:{{ rancher_port }}/v1/projects" return_content=yes
   register: project_id

- name: Return the registration token URL of Rancher server
  action: uri 
      method=POST
      status_code=201
      url="http://{{ rancher_server }}:{{ rancher_port }}/v1/registrationtokens?projectId={{ project_id.json['data'][0]['id'] }}" return_content=yes
  register: rancher_token_url

- name: Return the registration URL of Rancher server
  action: uri 
      method=GET 
      url={{ rancher_token_url.json['links']['self'] }} return_content=yes
  register: rancher_token

Then it will make sure that no other agent is running on the server and it will run the Rancher Agent:

- name: Check if the rancher-agent is running
  command: docker ps -a
  register: containers

- name: Register the Host machine with the Rancher server
  docker:
      image: rancher/agent:v{{ rancher_agent_version }}
      privileged: yes
      detach: True
      volumes: /var/run/docker.sock:/var/run/docker.sock
      command: "{{ rancher_token.json['registrationUrl'] }}"
      state: started
  when: "{{ 'rancher-agent' not in containers.stdout }}"

MySQL and WordPress Roles

The two roles are using Ansible Docker’s module to run Docker images on the server, you will note that each Docker container will start with RANCHER_NETWORK=true environment variable, which will cause the Docker container to use Rancher’s managed network so that containers can communicate on different hosts in the same private network.

I will use the official MySQL and WordPress images, the MySQL image requires the MYSQL_ROOT_PASSWORD environment variable to start, you can also start it with default database and user which will be granted superuser permissions on this database.

- name: Create a mysql docker container
  docker:
      name: mysql
      image: mysql:{{ mysql_version }}
      detach: True
      env: RANCHER_NETWORK=true,
           MYSQL_ROOT_PASSWORD={{ mysql_root_password }}

- name: Wait a few minutes for the IPs to be set to the container
  wait_for: timeout=120

# The following tasks help with the connection of the containers in different hosts in Rancher
- name: Fetch the MySQL Container IP
  shell: docker exec mysql ip -o -4 addr list eth0 | awk '{print $4}' | cut -d/ -f1 |  sed -n 2p
  register: mysql_sec_ip

- name: print the mysql rancher's ip
  debug: msg={{ mysql_sec_ip.stdout }}

Note that role will wait for 2 minutes until to make sure that the container is configured with the right IPs, and then it will fetch the container’s secondary ip which is the ip used in Rancher’s network and save it to the mysql_sec_ip variable which will survive through the playbook, WordPress image on other hand will start with WORDPRESS_DB_HOST set to the ip of the mysql container we just started.

- name: Create a wordpress docker container
  docker:
      name: wordpress
      image: wordpress:{{ wordpress_version }}
      detach: True
      ports:
      - 80:80
      env: RANCHER_NETWORK=true,
         WORDPRESS_DB_HOST={{ mysql_host }}:3306,
         WORDPRESS_DB_PASSWORD={{ mysql_root_password }},
         WORDPRESS_AUTH_KEY={{ wordpress_auth_key }},
         WORDPRESS_SECURE_AUTH_KEY={{ wordpress_secure_auth_key }},
         WORDPRESS_LOGGED_IN_KEY={{ wordpress_logged_in_key }},
         WORDPRESS_NONCE_KEY={{ wordpress_nonce_key }},
         WORDPRESS_AUTH_SALT={{ wordpress_auth_salt }},
         WORDPRESS_SECURE_AUTH_SALT={{ wordpress_secure_auth_salt }},
         WORDPRESS_NONCE_SALT={{ wordpress_nonce_salt }},
         WORDPRESS_LOGGED_IN_SALT={{ wordpress_loggedin_salt }}

Managing Variables

Ansible defines variables in different layers, some of layers override the others, so for our case I added a default set of variables for each role to be used in different playbooks later, and added the currently used variables in the group_vars directory to override them.

├── group_vars
│   ├── all.yml
│   ├── nodes.yml
│   └── Rancher.yml
├── hosts
├── rancher.yml
├── README.md
└── roles
    ├── docker
    ├── mysql_docker
    ├── rancher
    ├── rancher_reg
    └── wordpress_docker

The nodes.yml variables will apply on the nodes group defined in the inventory file which contains the database and application servers, this file contains information used by mysql and wordpress containers:

---
rancher_server: "{{ hostvars['rancher']['ansible_ssh_host'] }}"

# MySQL variables
mysql_root_password: "{{ lookup('password', mysql_passwd_tmpfile + ' length=20 chars=ascii_letters,digits') }}"
mysql_passwd_tmpfile: /tmp/mysqlpasswd.file
mysql_host: "{{ hostvars.node2.mysql_sec_ip.stdout }}"
mysql_port: 3306
mysql_version: 5.5

# WordPress variables
wordpress_version: latest

You may note that I used password lookup to generate a random password for mysql root password, a good alternative for this method would be vault  to encrypt sensitive data like passwords or keys.

Running the Playbook

To run the playbook, I fired up 3 machines with Ubuntu 14.04 installed and added their IPs to the inventory we saw earlier, and then used the following command to start the playbook:

$ ansible-playbook -u root -i hosts rancher.yml 

After the playbook finishes its work, you can access the Rancher server and you will see the following:

rancher_nodes

And when accessing the IP of node1 on port 80 you will access WordPress:

wordpress_rancher

Conclusion

Ansible is a very powerful and simple automation tool that can be used to manage and configure a fleet of servers, using Ansible with Rancher can be a very efficient method to start your environment and manage your Docker containers.  This month we are hosting an online meetup in which we’ll be demonstrating how to run microservices in Docker containers and orchestration application upgrades using Rancher.  Please join us for this meetup to learn more. 


REGISTER NOW



Free Online Training

Want a crash course in using Rancher?

Join us for free online training courses, hosted monthly by a Rancher technical expert. We provide a great hands-on overview for new users setting up a Rancher deployment, and answer any and all questions you have about Rancher and how to integrate it into your DevOps processes!

Sign up

Recent Posts


Upcoming Events