Part 2 - Provision with Ansible
Now that Terraform has taken care of standing up a single instance of EC2 and RDS/MariaDB in Part 1, the next part is pretty straight forward because we just need docker & docker-compose installed on our web server EC2 instance.
Just a reminder of the simple setup we're trying to build here:
Part 2. Provision with Ansible
All we're after here is for ansible to install docker & docker-compose on the target server. We could've done this with a user_data
section of our Terraform build so the new instance would come fully formed with docker & docker-compose. But again, even though this is a small and really simple setup, I wanted to practice structuring and implementing things in a scalable "industry standard" sort of way (well, close to anyway).
Directory Structure
Let's go through the ansible directory and see what we're working with
ansible.cfg
deploy.yml
hosts.yml
roles/
ansible.cfg
all your defaults go into this for ansible to pick up and work with when you run ansible
or ansible-playbook
.
[defaults]
remote_user = ubuntu
ansible_managed = Ansible managed
display_skipped_hosts = True
system_warnings = True
command_warnings = False
retry_files_enabled = False
self explanatory, or you can read and add as required, read this page: ansible.cfg example on github.com
hosts.yml
there's a couple ways to layout your hosts file, the ini way:
[web]
x.x.x.x hostname
x.x.x.x another-hostname
or the way I've decided to go, with YAML, the "ungrouped" way (per github inventory yaml example)
all:
hosts:
web:
ansible_ssh_host: 174.129.47.133
ansible_user: ubuntu
ansible_ssh_private_key_file: "/path/to/private/key/infra_builder.pem"
there's a few other ways to lay your inventory out with YAML, adding 'vars' and 'children', but these are some great practices to follow on inventory in YAML format:
# - Comments begin with the '#' character
# - Blank lines are ignored
# - Top level entries are assumed to be groups, start with 'all' to have a full hierarchy
# - Hosts must be specified in a group's hosts:
# and they must be a key (: terminated)
# - groups can have children, hosts and vars keys
# - Anything defined under a host is assumed to be a var
# - You can enter hostnames or IP addresses
# - A hostname/IP can be a member of multiple groups
the role of 'roles'
Now I'm no Ansible guru, but the more I'm learning about ansible the more powerful I see 'roles' being in terms of organizing the 'work' required to get from 'A' to 'B' for whatever you're trying to achieve.
In the example of this little project, we want docker & docker-compose installed.
Let's setup 'docker' as an Ansible role:
ansible-galaxy init docker
where 'docker' is the role we want to create. There's a way to pull down an already filled out 'docker' role much the way you pull down docker images, but its fun to start from scratch and see everything at the boilerplate level.
You end up with this folder structure (I copied this from ansible's documentation):
README.md
.travis.yml
defaults/
main.yml
files/
handlers/
main.yml
meta/
main.yml
templates/
tests/
inventory
test.yml
vars/
main.yml
Rather than get into an Ansible tutorial (which there are much better out there to learn from), let's just highlight the files that actually do stuff:
defaults/main.yml
---
# defaults file for docker
# this assumes either systemd or initd services
# uncomment the one you need:
## systemd
#docker_service: /usr/lib/systemd/system/docker.service
## initd
docker_service: /etc/init.d/docker
docker_install_docker_compose: True
docker_compose_version: "1.21.0"
key points
- pick the service system for your setup - systemd or init.d
- docker compose flag = True/False
- docker compose version to install
the main event, is tasks/main.yml. this is where docker and docker-compose get installed, and the docker service is enabled and started by this section:
--
# tasks file for docker
- name: check docker installed
stat: path={{ docker_service }}
register: install_result
- debug: var=install_result
- name: installing docker
shell: >
curl -fsSL https://get.docker.com/ | sh
when: not install_result.stat.exists
- name: start docker service
service: name=docker state=started enabled=true
- name: get docker_info
no_log: true
command: docker info
register: docker_result
- name: Install Docker Compose
get_url:
url: "https://github.com/docker/compose/releases/download/{{ docker_compose_version }}/docker-compose-Linux-x86_64"
dest: "/usr/local/bin/docker-compose"
force: True
owner: "root"
group: "root"
mode: "0755"
when: docker_install_docker_compose
key points:
- does the installation
- enables the docker service
- grabs docker_info and registers the result so it can be used for and by other sections
That's it. Now its time to deploy
deploy.yml
Pretty non-eventful after all the meat and potatoes of this is already done in the 'roles' section. The following just deploys our hard earned config setup:
# deploy.yml
---
- hosts: all
gather_facts: yes
become: yes
become_user: root
roles:
- { role: docker, tags: docker }
tasks:
- debug: var=docker_result
key points:
- targets 'all' hosts (i.e. the 'root' level so will capture our web hosts)
- the ansible_user will assume 'root' as it goes to work
- going to deploy the 'docker role' against the target hosts
- tag our docker role with 'docker' (not very creative)
Deploy, check Installation
That's it!
Third and Final Part III, where we setup our Docker Wordpress site with RDS backend!