The project is about using Ansible to deploy a multicontainer application to a distant cloud server.
- Install a virtual environment to get the proper ansible binary on your machine
./setup/env_install.sh && source ~/.zshrc
conda activate 42Cloud-$USER
- Create a vault_pwd file with the encryption passwrd in it (you should know it !)
echo -n "the_secret" > vault_pwd
-
Check that the host file containts valid public server addresses
-
Verify that you can connect as root with ssh to every server in hosts file
- In case you get the error "UNREACHABLE [...] Host key verification failed", use the following command to remove the concerned host from your known hosts file:
ssh-keygen -f "/mnt/nfs/homes/$USER/.ssh/known_hosts" -R "XX.XXX.XX.XXX"
-
Launch ansible playbook
ansible-playbook site.yml
ansible-playbook site.yml -e "reset=true" # clear the application
ansible-playbook site.yml --tags webapp # select some tasks
- As we are working on shared school machines, we used Conda in this project to setup a virtual environment containing all the necessary packages (python and ansible + dependencies).
In order for the environment to be easy to setup, this project contain a setup script in /setup
:
./setup/env_install.sh && source ~/.zshrc
conda activate 42Cloud-$USER
- the main playbook
site.yml
is located at the root of the project and execute different sets ofplays
named roles (see more in Ansible roles below). - the host file references all the hosts we worked with (no longer viable) under the name
webservers
. We call them all at once when play the playbook by default. - We can also call individually the execution of the playbook on one host using the option
--limit $HOSTNAME
- Servers can also be called using different group names when set up in the hosts file. For instance, we could have set up the groups
scaleway
andgoogle
instead ofwebservers
to call one group or the other separately.
- in the main playbook
site.yml
, the different roles are executed all together when playing the playbook. However, we addedtags
in order to call only one play at a time. For instance, to relaunch the webapp role only, we used--tags webapp
The main concept we used in Ansible is the Task. A task is a single command you want to execute on a remote host.
The tasks are working with modules which allow you to execute specific actions on a machine (like copying a file, creating a folder, launching a service, ...)
List of supported modules : All modules
Examples of module with their attributes :
- name: start nginx
service:
name: nginx
state: started
- name: Add a user for ansible tasks
user:
name: "admin"
comment: "Ansible admin}"
shell: "/bin/bash"
create_home: yes
state: present
The modules usually have a state. The state is defining the state we want to have the resource by the action of the module. It is recommanded by the Ansible doc to always specify the state if it is possible, even if the default value fits.
Example of states :
- File : present / absent
- Apt : present / absent
By using a mudule, we are just specifying in which state we want the resource to be and Ansible will "translate" this to the necessary chain of commands in order to achieve the proper state. If your state s 'present' and the file you want to create is already here, it will consider the state as ok and go to the next task without having an unnecessary action.
It is recommanded to organize tasks in roles so they are easier to manage and reuse. This is explained in the next part.
However, you have the possibility to split playbooks that are to huge into 'sub-playbooks' that can be imported onto a global one.
Here is an example from the project :
main.yml
---
# Prerequisites
- include_tasks:
- include_tasks: main/02_env_file_update.yml
- include_tasks: main/03_bind_folders.yml
# down the app (and optionally reset it)
- include_tasks: main/04_down_reset.yml
# buildand launch containers
- include_tasks: main/05_deploy.yml
# configuring crontab to launch task on startup
- include_tasks: main/06_launch_on_reboot.yml
main/01_file_copy.yml
---
# create a directory named /app
- name: create directory /app
become: yes
become_user: root
file:
path: /app
owner: "{{ ansible_user }}"
state: directory
# copy files from controler
- name: copy webapp files
synchronize:
src: "{{ role_path }}/files/webapp_files/"
dest: "/app"
delete: yes
vars:
ans_user: "{{ ansible_user }}"
delegate_to: localhost
...
You have the possibility to use several general 'options' on tasks. Here are the ones we used in the project :
- register : to register the output of a task so it can be used as a variable in other tasks
- name: get the current user
shell: echo $USER
register: current_user
- name: add the current user in docker group
become: yes
become_user: root
user:
name: "{{ current_user.stdout }}"
groups: docker
append: yes
state: present
- become / become_user : to execute a distant task as a specific user
- name: create directory /app
become: yes
become_user: root
file:
path: /app
owner: "{{ ansible_user }}"
state: directory
- delegate_to : to execute the task on a certain host (even on localhost)
- name: copy webapp files
synchronize:
src: "{{ role_path }}/files/webapp_files/"
dest: "/app"
delete: yes
vars:
ans_user: "{{ ansible_user }}"
delegate_to: localhost
- with_items : to loop over the items of a list
- name: create database and wp bind folder
become: yes
become_user: root
file:
path: "{{ bind_folder }}/{{ item }}"
owner: "{{ ansible_user }}"
group: "{{ ansible_user }}"
state: directory
with_items:
- db
- wordpress
The concept of role in Ansible allows to categorize the hosts based on their intended purpose.
It is an "organization" feature that makes the whole thing cleaner and easier to manage. Below is the classic structure of a role according to ansible doc :
|── common/ # this hierarchy represents a "role"
| |── tasks/
| | └── main.yml # <-- tasks file can include smaller files if warranted
| |── handlers/
| | └── main.yml # <-- handlers file (repetitive tasks that can be called by other tasks)
| |── templates/ # <-- files for use with the template resource to configure systems
| | └── ntp.conf.j2 # <------- templates end in .j2
| |── files/
| | |── bar.txt # <-- files for use with the copy resource
| | └── foo.sh # <-- script files for use with the script resource
| |── vars/
| | └── main.yml # <-- variables associated with this role
| |── defaults/
| | └── main.yml # <-- default lower priority variables for this role
| |── meta/
| | └── main.yml # <-- role dependencies
| |── library/ # roles can also include custom modules
| |── module_utils/ # roles can also include custom module_utils
| └── lookup_plugins/ # or other types of plugins, like lookup in this case
└──
- There two directories in each role dedicated to variables, default and vars. The
default
file is used to store default values for having a fallback value. Thevars
file is used to set up variables that will be used in the plays. When set up,vars
variables overwritedefault
values.
- The folder files is used to store files that are used in the plays, such as files that we copy to the servers or the encrypted .env file which is not copied to the server but read by Ansible using the encryption key (see more in Ansible vault).
- The templates folder is used to store template of scripts that will be used for setup on each machine when they need specific data of the server. As this value will be replaced at the execution, we template the script and store them here.
- The lookup plugins from Ansible are provided to execute functions and access data from the controller machine or outside sources (database, APIs...) in contrary to the other function which are played on the remote server. We used it to read the public key or to read the encrypted variables executing ansible vault decryption.
- Ansible vault is a service to handle security concerns around sensitive data such as passwords that may be used within the playbook.
- As we have a .env file used by docker-compose, we encrypted the file on our controller machine using the
ansible-vault encrypt
command. We created a local file named vault_pwd at the root of the project containing our encryption key and provided this file within the playwebapp
. The .env file remains always encrypted on the controller machine and is only read by Ansible at execution. The value of the variables are then passed to the server machine where an .env file is created to be used by Docker there.
- we could have added roles dedicated to each container in order to administrate them separately from our controller machine using Ansible (logs, stop, restart...). It would enable us to check container's state without connecting to the remote server.
Sometimes you want a task to run only when a change is made on a machine. For example, you may want to restart a service if a task updates the configuration of that service, but not if the configuration is unchanged.
In this project, we could have use this to reload ssh on the remote server only if the authorized_keys
file changed.
Here is the command to execute that. Ansible use notify to reference and call the handler :
in tasks/main.yml
- name: Create authorized_keys file
lineinfile:
dest: "/home/{{ user }}/.ssh/authorized_keys"
line: "{{ public_key }}"
state: present
create: yes
notify:
- Restart ssh
- name: Ensure ssh is running
service:
name: ssh
state: started
in handlers/main.yml
handlers:
- name: Restart ssh
service:
name: ssh
state: restarted
In a playbook, you may want to execute different tasks, or have different goals, depending on the value of a fact (data about the remote system), a variable, or the result of a previous task.
In this project, it could have been helpful to limit the changes when playing the playbook (test a thing in a task and execute the next task according to the result of the first task), or to be more flexible about some facts on the remote server (OS for example).
Here is a classic example of conditional task based on an ansible fact. In a task, you can use when to specify a condition :
tasks:
- name: Shut down Debian flavored systems
ansible.builtin.command: /sbin/shutdown -t now
when: ansible_facts['os_family'] == "Debian"
👩 Estelle RECUERO
- Github: @estelle-rcr
👨 Thomas WAGNER
- Github: @twagger