diff --git a/README.md b/README.md index 652e76be82a1be787da58dab3208683d6d1a663d..b5f829ccdbad8a281ec12bb88514b07f88ffebda 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ -# Lab: Configuration Management - Ansible and AWS +# Lab: Configuration Management - Ansible -Lab template for a CM/deployment exercise with Ansible and AWS. +Lab template for a CM/deployment exercise with Ansible and AWS. Originally +written by Marcel Graf (HEIG-VD). ## Pedagogical objectives ## @@ -24,10 +25,10 @@ conventions about the *command line prompt*: * `ins`: your VM instance -### Task 1: install Ansible ### +### Task #1: install Ansible ### -In this task you will install [Ansible](https://www.ansible.com/) on your -local machine. Please, refer to your [OS +**Goal:** install [Ansible](https://www.ansible.com/) on your local +machine. Please, refer to your [OS documentation](https://docs.ansible.com/ansible/latest/installation_guide/intro_installation.html) for the proper way to do so. @@ -37,62 +38,63 @@ Once done, verify that Ansible is installed correctly by running: lcl$ ansible --version ``` -### Task 2: Create a VM on Amazon Web Services ### +### Task #2: Create a VM on a Cloud of your choice ### -In this task you will create a VM on Amazon Web Services that will be managed -by Ansible. +**Goal:** create a VM that will be managed by Ansible. Chose any Cloud you are +familiar with, then: + 1. Import or create an RSA key pair for SSH access to the VM. - 1. Switch the AWS console to the N. Virginia region to avoid resource - limitations -- see "Zones" in the "Account attributes" pane. + 1. Create a security group/policy that allows incoming SSH, HTTP and HTTPS + traffic from anywhere (0.0.0.0/0). - 2. Import or create an RSA key pair in this region and download the private key. - - 3. It it doesn't exist yet, create a security group named `Lab-Ansible-AWS` - group that allows incoming SSH, HTTP and HTTPS traffic from anywhere - (0.0.0.0/0). - - 4. Create an EC2 instance with the following characteristics (all other - parameters at default value): + 1. Create a VM instance with the following characteristics: - OS: Ubuntu Server 20.04 LTS - - type: t2.micro - - security group: Lab-Ansible-AWS + - type: the smallest capable of running the above OS. 1 core, 1GB RAM, + 10GB virtual disk should be enough. + - security group/policy: the one you created above - key pair: the one you created above -After launching make sure you can SSH into the VM using your private key -(`<your-private-key>` is a full path): +After launching make sure you can SSH into the VM using your +`<your-private-key>` (must be a full path): ``` shell lcl$ ssh -i <your-private-key> ubuntu@<VM-DNS-name-or-IP-address> ``` -### Task 3: Configure Ansible to connect to the managed VM ### - -In this task you will tell Ansible about the machines it shall manage. +### Task #3: Configure Ansible to connect to the managed VM ### -Create a "sandbox" directory on your local machine f.i. `~/ansible/playbooks`, -and create a file called `hosts.yml` which will serve as the inventory file, -and add the following: +**Goal:** intruct Ansible about the machines (hosts) it shall manage. -@@@ RESTART FROM HERE @@@ +Create a "sandbox" directory on your local machine f.i. `~/ansible/`, and +create a file called `hosts.yml` which will serve as the *inventory* file +(a.k.a. hostfile; it can also be written in `.ini` style), and add the +following: ``` yaml -testserver ansible_ssh_host=<VM-DNS-name-or-IP-address> - ansible_user=ubuntu - ansible_ssh_private_key_file=<your-private-key> +all: + hosts: + testserver: + ansible_ssh_host: <VM-DNS-name-or-IP-address> + ansible_user: ubuntu + ansible_ssh_private_key_file: <your-private-key> ``` -Verify that you can use Ansible to connect to the server: +and check its validity: -ansible testserver -i hosts -m ping +``` shell +ansible-inventory -i ~/ansible/hosts.yml --list +``` -You should see output similar to the following: +Verify that you can use Ansible to connect to the testserver: -testserver | SUCCESS => { - "changed": false, - "ping": "pong" -} +``` shell +lcl$ ansible -i ~/ansible/hosts.yml testserver -m ping +``` +You should see output similar to the following: + +` testserver | SUCCESS => { "ansible_facts": { "discovered_interpreter_python": "/usr/bin/python3" @@ -100,35 +102,264 @@ testserver | SUCCESS => { "changed": false, "ping": "pong" } +` -We can now simplify the configuration of Ansible by using an ansible.cfg file which allows us to set some defaults. - -In the playbooks directory create the file ansible.cfg: +Let's simplify the configuration of Ansible by using a user default +configuration file `~/.ansible.cfg` with contents (`.ini` style): +``` ini [defaults] -inventory = hosts +inventory = ~/ansible/hosts.yml remote_user = ubuntu -private_key_file = <path to keyfile.pem> +private_key_file = <your-private-key> host_key_checking = false deprecation_warnings = false +``` + +Notice that SSH's host key checking is disabled for convenience, as the +managed VM will get a new IP address each time it is recreated. **For +production systems this is be a security risk!** [See the +doc](https://docs.ansible.com/ansible/latest/reference_appendices/config.html#the-configuration-file) +for the other details. + +With the above default settings our inventory file now simplifies to: + +``` yaml +all: + hosts: + testserver: + ansible_ssh_host: <VM-DNS-name-or-IP-address> +``` -Among the default options we also disable SSH's host key checking. This is convenient when we distroy and recreate the managed server (it will get a new host key every time). In production this may be a security risk. +Now calling ansible is simpler: -We also disable warnings about deprecated features that the 2.x version of Ansible emits. +``` shell +lcl$ ansible testserver -m ping +``` -With these default values the hosts inventory file now simplifies to: +The ansible command can be used to run arbitrary commands on the remote +machines. F.i., to execute the uptime command: -testserver ansible_ssh_host=<managed VM's public IP address> +``` shell +lcl$ ansible testserver -m command -a uptime +testserver | CHANGED | rc=0 >> + 09:05:10 up 54 min, 2 users, load average: 0.01, 0.00, 0.00 +``` -We can now run Ansible again and don't need to specify the inventory file any more: -ansible testserver -m ping +### Task #4: Install web application ### -The ansible command can be used to run arbitrary commands on the remote machines. Use the -m command option and add the command in the -a option. For example to execute the uptime command: +**Goal:** configure the managed host to run a simple Web application served by +the nginx server. This necessitates four files: -ansible testserver -m command -a uptime + 1. The inventory file `~/ansible/hosts.yml` as written before. + 2. A "playbook" that specifies what to configure + `~/ansible/playbooks/web.yml`. + 3. The nginx's configuration file `~/ansible/playbooks/files/nginx.conf`. + 4. A home page template for our Web site + `~/ansible/playbooks/templates/index.html.j2`. -You should see output similar to this: +To make our playbook more generic, we will use an ansible group called +`webservers` holding our managed server, so that we can then later easily add +more servers which Ansible will configure identically. Modify the hostfile by +adding a definition of the group webservers, this is **left to you as an +exercise**. If you do it correctly, you should get the following inventory +list: -testserver | CHANGED | rc=0 >> - 18:56:58 up 25 min, 1 user, load average: 0.00, 0.01, 0.02 +``` shell +lcl$ ansible-inventory --list +{ + "_meta": { + "hostvars": { + "testserver": { + "ansible_ssh_host": ... + } + } + }, + "all": { + "children": [ + "ungrouped", + "webservers" + ] + }, + "webservers": { + "hosts": [ + "testserver" + ] + } +} +``` + +You should now be able to ping the webservers group: + +``` shell +lcl$ ansible webservers -m ping +``` + +The output should be the same as before. + +Now, create a playbook named `~/ansible/playbooks/web.yml` with the following content: + +``` yaml +--- +- name: Configure webserver with nginx + hosts: webservers + become: True + tasks: + - name: install nginx + apt: name=nginx update_cache=yes + + - name: copy nginx config file + copy: src=files/nginx.conf dest=/etc/nginx/sites-available/default + + - name: enable configuration + file: > + dest=/etc/nginx/sites-enabled/default + src=/etc/nginx/sites-available/default + state=link + + - name: copy index.html + template: src=templates/index.html.j2 dest=/usr/share/nginx/html/index.html mode=0644 + + - name: restart nginx + service: name=nginx state=restarted +``` + +:question: How many tasks are declared in the above playbook? + +Then, create the nginx's configuration file +`~/ansible/playbooks/files/nginx.conf` referenced in the playbook references, +which the following content: + +``` nginx +server { + listen 80 default_server; + listen [::]:80 default_server ipv6only=on; + + root /usr/share/nginx/html; + index index.html index.htm; + + server_name localhost; + + location / { + try_files $uri $uri/ =404; + } +} +``` + +As per the aboveconfiguration file, nginx will serve the Web app's homepage +from `index.html`. This will be generated via Ansible's templating engine from +a template, which has to be created as +`~/ansible/playbooks/templates/index.html.j2` and should hold the following: + +``` html +<html> + <head> + <title>Welcome to ansible</title> </head> + <body> + <h1>nginx, configured by Ansible</h1> + <p>If you can see this, Ansible successfully installed nginx.</p> + <p>{{ ansible_managed }}</p> + <p>Some facts Ansible gathered about this machine: + <table> + <tr><td>OS family:</td><td>{{ ansible_os_family }}</td></tr> + <tr><td>Distribution:</td><td>{{ ansible_distribution }}</td></tr> + <tr><td>Distribution version:</td><td>{{ ansible_distribution_version }}</td></tr> + </table> + </p> + </body> +</html> +``` + +Now, run the newly created playbook to install and configure nginx, and to +deploy the Web app on the managed host: + +``` shell +lcl$ ansible-playbook ~/ansible/playbooks/web.yml +``` + +If everything goes well, the last output lines should be like: + + PLAY [Configure webserver with nginx] ************************************** + + TASK [Gathering Facts] ***************************************************** + ok: [testserver] + + TASK [install nginx] ******************************************************* + changed: [testserver] + + TASK [copy nginx config file] *********************************************** + changed: [testserver] + + TASK [enable configuration] ************************************************* + ok: [testserver] + + TASK [copy index.html] ****************************************************** + changed: [testserver] + + TASK [restart nginx] ******************************************************** + changed: [testserver] + + PLAY RECAP ****************************************************************** + testserver : ok=6 changed=4 unreachable=0 failed=0 + + +Point your Web browser to the address of the managed server (mind that we are +not using SSL): `http://<VM-DNS-name-or-IP-address>`. You should see the +homepage showing "nginx, configured by Ansible". + + +### Task #5: Test Desired State Configuration principles ### + +**Goal:** test and verify that Ansible implements the principles of Desired +State Configuration. + +According to this principle, before doing anything, Ansible should establish +the current state of the managed server, compare it to the desired state +expressed in the playbook, and then only perform the actions necessary to +bring the current state to the desired state. + +In its ouput, Ansible marks tasks where it had to perform some action as +*changed* whereas tasks where the actual state already corresponded to the +desired state as *ok*. + + 1. Return to the output of the last ansible command that run the `web.yml` + playbook. + 1. :question: There is one additional task that was not in the + playbook. Which one? + 1. :question: Among the tasks that are in the playbook there is + one task that Ansible marked as *ok*. Which one? Do you have a possible + explanation? + 1. Re-run the `web.yml` playbook a second time and compare its output with + the first run's. In principle nothing should have changed. + 1. :question: Which tasks are marked as changed? Why? + 1. SSH into the managed server. Modify the nginx configuration file + `default` (by the way, what's the deployment path?), for example by + adding a line with a comment. Re-run the playbook. + 1. :question: What does Ansible do to the file and what does it show in + its output? + 1. Do something more drastic like completely removing the homepage + `index.html` (by the way, what's the deployment path?) and repeat the + previous question. + 1. :question: What happened this time? + + +### Task #6: Adding a handler for nginx restart ### + +**Goal:** improve the playbook by restarting nginx only when needed. + +The current version of the playbook restarts nginx every time the playbook is +run, irrespective of the managed server's state. This goes indeed a bit too far. + +By putting the nginx restart command into a *handler*, instead of a task, its +execution can be made conditional. The rationale is that nginx is restarted +only if one of the tasks that affects nginx's configuration resulted in a +change. + +Consult the [Ansible documentation about +handlers](https://docs.ansible.com/ansible/latest/user_guide/playbooks_handlers.html). Modify +the playbook so that the nginx restart becomes a handler and the tasks that +potentially modify its configuration use *notify* to call the handler when +needed. + +Copy the modified playbook into the lab report.