Skip to main content

Project 1: Ansible

Deze repo bevat de opgave voor een project volgend op de workshop van Ansible.

Doelstelling

In dit project:

  • toon je aan dat je verschillende systeembeheertaken kan uitvoeren met Ansible
  • Pas je zo veel mogelijk de best practices toe om Ansible-taken uit te voeren.

Project - Opgave

Stap 1 - Deploy instantie / Voorbereidingen

Voor dit project werk je met een aparte deployment die voorzien werd op de private cloud van onze opleiding.

  • Je progressie en uiteindelijke oplossing komt terecht in een repo met de naam 2324-IaC_Ansible_Project-Familienaam Voornaam

  • Deploy een nieuwe instantie "OPO_IaC_Ansible_Project" op de private cloud en geef deze de naam IaC-Ansible-Project-Familienaam Voornaam.

  • Schema: schema

  • Noteer je ip-adressen: De ip-adressen kan je best hieronder noteren.

Virtual MachineOSIP-adrescredentials
Ansible Control HostRockystudent/student
Managed Host 1Ubuntuubuntu/Azerty123
Managed Host 2Ubuntuubuntu/Azerty123
Managed Host 3Rockystudent/student
RouterCisco CRS1000vcisco/cisco123!
  • Neem na opstart een snapshot van elke VM. Op die manier kan je in een latere fase makkelijk teruggrijpen naar een propere versie van je VM.
tip

Neem eventueel tussendoor ook nog eens een snapshot vooraleer ingrijpende aanpassingen door te voeren. Of... door een snapshot terug te zetten kan je ook je playbook meerdere keren toepassen en testen alsof het de eerste keer is.

  • Zorg er voor dat je op een makkelijke manier de Control Host kan gebruiken. Voorzie je connectie met de Control Host via MobaXterm of nog beter, via een Remote-SSH-connectie bij Visual Studio Code of gelijkaardig.

Stap 2 - Ansible voorbereiding

  • Installeer Ansible op je Control Host

  • Voorzie Password-less SSH-Authentication met alle nodes (host1, host2 en host3) behalve de router (wél met wachtwoord) en de Ansible Control Host (lokale authenticatie).

  • Alle Playbooks, Roles, inventories, variabelen, etc... komen terecht in een subfolder van je home-folder "student". Deze subfolder heet ansible-project.

Stap 3 - Inventory

  • Voorzie een inventory met volgende groepen:
@all:
|--@controlhosts:
| |--controlnode1 => Ansible Control Host
|--@dbservers:
| |--host3 => Load Balancer en Database server
|--@loadbalancers:
| |--host3 => Load Balancer en Database server
|--@routers:
| |--CSR1000v => router
|--@ungrouped:
|--@webservers:
| |--webnode1 => host1: webserver
| |--webnode2 => host2: webserver
  • Uiteraard worden variabelen (cfr. Best Practices) niet in de inventory opgenomen maar in aparte en beveiligde bestanden.
note

Idealiter splits je de "gevoelige" data af van de gewone variabelen en worden deze apart geëncrypteerd opgeslagen.

tip

Eventueel kan je nog wachten om je bestanden met gevoelige data te beschermen om op dit moment nog wat makkelijker te kunnen werken. Maar op het einde van je opgave moet dit uiteraard wel in orde gebracht zijn.

  • Voorzie eventueel nu reeds je basis-structuur in de folder van je project.

Stap 4 - Uitvoering van enkele standaardrollen

In onderstaand blok worden verschillende verwachtingen geformuleerd.
Het is aan jou om deze zelf op een zo goed mogelijke manier te implementeren rekening houdend met best practices.

We zullen het geheel zo veel mogelijk in rollen proberen uit te voeren zodat je heel gestructureerd je Ansible-taken kan uitvoeren.

Probeer ook waar mogelijk zo efficiënt mogelijk te werken (loops, conditionals,...)

Taak 0 - Playbook

In de volgende taken wordt gevraagd om alles netjes in rollen uit te werken. Uiteraard moeten deze rollen aangesproken worden door deze op te nemen in een playbook.

  • Zorg er, bij het uitvoeren van onderstaande taken voor, dat je een 'playbook' met de naam playbook_voornaam.yml maakt met daarin verschillende plays om de rollen toe te passen op de verschillende servers, routers,...
note

Op die manier kan je telkens ook stap per stap controleren of je playbook en je rollen hun taken correct uitvoeren.

Taak 1 - Common

  • Creëer een nieuwe rol met de naam common die je nodes een software update/upgrade geeft en dus alle pakketten van je OS (zowel Ubuntu als Centos) op de recentste versie brengt (eventueel ook de package repository updaten!).
tip

De allereerste keer kan het heel lang duren vooraleer je playbook volledig uitgevoerd wordt als er heel veel updates moeten gebeuren. Heb even geduld of voer update eerst eens manueel uit.

  • Voorzie in dezelfde rol dat de firewall actief is én een SSH-connecties toelaat.
tip

Herstart indien nodig de firewall om de regels in werking te stellen. Maak gebruik van Handlers.

Taak 2 - Webservers

  • Maak van de webserver-nodes effectief webservers. We gebruiken Nginx als webserver-service. Voorzie hiervoor ook een nieuwe rol webserver.

  • Bij opstarten van de nodes moet de webservice automatisch opgestart worden.

  • Voorzie in diezelfde rol dat de correcte poorten in de firewall openstaan om connecties met de webservers via http én https toe te laten.

Controle

Tussentijds kan je best controleren of je kan surfen naar de default-pagina van Nginx vooraleer verder te werken en andere rollen uit te werken.

Taak 3 - Database server

node3 zal zowel als Load Balancer fungeren én als Database server.

  • Voorzie een nieuwe Ansible-rol met de naam database waarmee mysql-server geïnstalleerd wordt op node3.
tip
  • Zorg dat de service gestart is, ook na een reboot.
  • Wellicht zal je een extra python-gerelateerd packet moeten installeren (bekijk goed je foutmeldingen)
  • Voorzie in diezelfde rol dat de firewall connecties met deze databaseserver toelaat als deze afkomstig zijn van de webservers.
    Je hoeft dus geen rekening te houden met de poorten. Aan de firewallregels van je database-server voeg je dus de source ip-adressen (publieke netwerk) van je webservers toe aan de zone Public (ip's op een dynamische manier en dus niet "hard coded").

  • Voeg een nieuwe mysql-gebruiker toe waarmee we later in de mysql-databank kunnen aanmelden. Gebruik als nieuwe user je eigen voornaam en kies een bijhorend wachtwoord dat je, cfr best practices, apart als variabele opslaat.

note

Idealiter zorg je ervoor dat wachtwoorden niet verschijnen bij het uitvoeren van je play in de output of opgenomen worden in logs.

  • Creëer in je mysql-server een nieuwe databank met de naam voornaamfamilienaam (aan te passen met je eigen naam uiteraard).

  • Voer nu ook via deze rol een query uit op je mysql-server:

    • Je weet dat via een gewone command-line deze query kan uitgevoerd worden met mysql --user=username --password=xxxx --execute="select ....;".
    • de query vraagt een lijst van de users uit de tabel user van de database mysql. Neem de velden user, host en password_last_changed op in de query.
    • Registreer de output van deze query in een variabele.
    • Toon, bij het uitvoeren van je playbook, het resultaat van dit command (dat in een variabele zit) op het scherm.
Controle

Wil je tussendoor eens testen of je Playbook wel het gewenste resultaat oplevert? Dan kan je eventueel nu als test eens manueel proberen om een connectie te maken vanaf een webnode naar de mysql databank met de gebruiker die je voorzien hebt. Om dat te kunnen doen zal je dan wel even de msql-client manueel moeten installeren op je webnode. Dit pakket werd immers niet opgenomen in de vereiste packages (en moet er ook niet in...).

mysql -h <ip_van_de_db_server> -u <aangemaakte_user> -p

Taak 4 - Loadbalancer

Aangezien we 2 webservers hebben die dezelfde content aanbieden kunnen we daarvoor een loadbalancer plaatsen die requests verdeelt over beide nodes.

  • Voorzie een nieuwe rol loadbalancer die de package installeert om Nginx te implementeren.

  • De configuratie van de loadbalancer moet aangepast worden:

    • Maak van dit bestand een template in Ansible waarin je volgende parameters dynamisch maakt (variabelen) die @runtime door Ansible ingevuld worden:
      • de poort waarop de loadbalancer luistert
      • het ip-adres van de backendservers
    • Plaats uiteraard de template en de variabelen op de juiste plaats in je Ansible-project.
belangrijke tips
  • uitleg over de configuratiefile van nginx kan je makkelijk online vinden (vb https://docs.nginx.com/nginx/admin-guide/load-balancer/http-load-balancer/ of ...)

  • Vermoedelijk zal SELinuxin de weg zitten om je load balancer requests te laten doorsturen naar upstream nodes. SeLinux is een ingebouwde beveiligingsarchitectuur, die anders dan een firewall nog veel meer de interne processen controleert.
    Je zal wellicht de nginx-service, in SELinux httpd_t genaamd, de nodige permissive toegang moeten geven. Dit kan je in deze situatie ook makkelijk met een Ansible-module doen (community.general.selinux_permissive).

  • De module om nginx permissive rechten te geven in SELinux heeft ook enkele python dependencies. Die zijn in principe al automatisch mee geïnstalleerd als je de vorige stappen geïmplementeerd hebt. Mocht je die vorige stappen overgeslagen hebben, dan zal je alsnog in deze fase ook de package "policycoreutils-python-utils" moeten installeren. Dus sowieso is het niet slecht om in een rol, die eigenlijk volledig onafhankelijk moet kunnen functioneren, de dependencies te installeren. Je weet immers nooit als deze reeds aanwezig zijn of niet.

Controle

Op dit moment kan je controleren of je load balancer wel degelijk zijn connecties aanvaardt en doorstuurt naar de achterliggende nodes.
Je kan uiteraard met je browser eens surfen naar het ip-adres van de load balancer. Je zou dan, zoals te verwachten, de standaard pagina's van de nginx-webserver moeten te zien krijgen.
Omdat het moeilijk is om te weten welke van de twee webnodes nu het antwoord geeft, kan je aan de configuratie van je load balancer (nginx.conf) nog een extra regel meegeven:

add_header X-Backend-Server $upstream_addr;

Deze regel zorgt er voor dat er een extra header toegevoegd zal worden in de response met het ip-adres van de webserver die de pagina teruggaf. Op die manier moet je, met wat refreshen, duidelijk zien (via F12 in je browser) dat er gewisseld wordt tussen beide nodes.

Taak 5 - Diverse

  • Maak een nieuwe rol aan met de naam varia.

  • Zorg in deze rol dat de hosts-file van zowel je webservers (webnode1 en webnode2), je databaseserver (host3) én de Control Host (controlnode1) aangevuld wordt met de nodige regels om de hostnames van de webservers, de loadbalancer en de router te kunnen resolven naar hun ip-adres. Een ping naar "host3" moet dan prima werken vanop elke Linux-VM van je opstelling.

  • Zorg er bijkomend voor dat de hostname de naam van de node in de inventory krijgt. Dus i.p.v. de hostnaam die bepaald werd door de virtualisatieomgeving krijgen je machines de naam uit de inventory.

tip

Een gewijzigde hostname is niet altijd meteen zichtbaar op de bash prompt. Controleren kan met het commando hostname.

Taak 6 - Router

Met Ansible kan je heel veel verschillende devices beheren. In deze taak maken we met Ansible connectie met een Cisco IOS Router (CSR1000v).

  • Maak een nieuwe rol aan met de naam routerconfig.
    • Pas de "hostname" aan naar routervoornaam, vb. routersven.
    • Neem een backup van de running-config en sla deze op in een submap backup in je Ansible-projectfolder. (Zorg ervoor dat deze map "backup" achteraf ook aanwezig is in je repo op Gitlab samen met je volledige Ansible project.
tip

Om een connectie te kunnen maken met de cisco-router (via ssh) zijn er twee zaken waar je rekening mee moet houden:

  • De klassieke module die Ansible gebruikt om ssh-connecties te maken kan niet gebruikt worden voor network-devices zoals bv. Cisco routers. Je zal dus even moeten opzoeken welke alternatieve connectiemethode je kan gebruiken in Ansible (het blijft ook gestoeld op ssh maar is minder uitgebreid).

  • helaas ondersteunt de CSR1000V router die wij gebruiken niet de nieuwste beveiligingstechnieken en anderzijds passen de recentste versies van Red Hat gebaseerde distros (zoals Rocky) een strenge security-policy toe. In dit geval zorgt dit voor problemen omdat de "Key Exchange Algoritms" van de router niet meer standaard aanvaard worden door de ssh-client op je Linux VM.
    Op je client passen we daarvoor twee zaken aan:

    • in het bestand ~/.ssh/conf zet je het volgende (bestand eventueel aanmaken):

      Host *
      KexAlgorithms +diffie-hellman-group-exchange-sha1,diffie-hellman-group14-sha1
      HostKeyAlgorithms +ssh-rsa
    • de cryptografische policies "verlagen" van DEFAULT naar LEGACY met volgend commando:

      update-crypto-policies --set LEGACY

      Daarna best even de sshd-service herstarten met sudo systemctl restart sshd

Stap 5 - Uitvoering van rollen voor App-Deployment

Op dit moment van de opgave wordt enkel de standaard webpagina van nginx getoond.

Het kan veel interessanter worden als we een echte webapplicatie zouden kunnen gaan uitrollen die bv. ook gebruik maakt van een databank. Tegenwoordig bestaat er een overaanbod aan frameworks in verschillende programmeertalen, zowel voor front- als backend.

In deze cursus ligt de focus zeker niet op het ontwikkelen van applicaties of op de mooie weergave daarvan. Maar via IaC moet het wel mogelijk zijn om dergelijke applicaties te deployen en volledig operationeel te maken.

We kozen in deze opgave om een webapplicatie te deployen die gemaakt is met het framework Django. Django is een vrij populair en zeer veelzijdig framework dat gebaseerd is op python. Het wordt voornamelijk gebruikt als backend maar ook de layout kan in principe wel in Django zelf voor een mooi geheel zorgen. Toch wordt Django in de praktijk dikwijls gekoppeld aan echte frontend-frameworks zoals Vue, React,...

Opzet

Met Django kan je snel een webapplicatie bootstrappen die de volledige mappenstructuur voor je project aanmaakt en onmiddellijk functioneel is. Er is in principe ook een development webserver aanwezig waardoor ontwikkelaars snel het resultaat kunnen bekijken van hun applicatie. In principe zou het ook mogelijk zijn om die development server open te zetten en te laten gebruiken door de publieke wereld. Maar daar is deze interne webserver niet voor bedoeld en vooral niet performant en veilig genoeg.

Django Development server
python3 manage.py runserver

Een Django applicatie zal in productie typisch gebruik maken van een echte performante webserver (Nginx, Apache,...) in combinatie met python interface. Aangezien de klassieke webservers geen python "spreken", is er een extra interface nodig tussen enerzijds het Django-framework (python) en anderzijds de webserver (html). Er zijn momenteel twee interfaces die Django ondersteunt, WSGI en ASGI.
In ons project maken we gebruik van Gunicorn, wat een typische, pure Python WSGI server is.

gunicorn

In onze situatie, waar we reeds een load balancer voorzien hebben om onze webnodes redundant te hebben, is de situatie dan als volgt:

gunicorn_lb

Stap 1 - Django project binnenhalen / Python Virtual Environment

Het installeren van deze volledige Django applicatie zullen we in Ansible aanpakken met een nieuwe rol: django_app.

tip

Maak zoveel mogelijk gebruik van variabelen in de volgende rollen. Op die manier kunnen deze rollen makkelijk op andere plaatsen ingezet worden en zitten bv paden niet "ingebakken".
=> Zorg op deze manier dat het pad, waar de applicatie zal geplaatst worden, in een variabele home_folder zit.

Django project binnenhalen en rechten instellen.
  • In deze eerste stap moet een voorbeeldapplicatie van gitlab, Django Applicatie, binnengehaald (gekloond) worden en in de /var/www/iac_app/-folder geplaatst worden.

  • Omdat straks de nginx-service deze bestanden moeten kunnen bereiken moeten de rechten op deze volledige folder aangepast worden. Dit kan op twee manieren. Ofwel gebruik je een ansible-module om de rechten en het ownership aan te passen naar de gebruiker www-data ofwel voer je de vorige taak uit in naam van de gebruiker www-data ipv de huidige gebruiker (ubuntu).

    repo permissions

info

In de volgende stappen is het dikwijls van belang welke rechten je hebt op de doelhosts om commando's (modules) uit te voeren. Nu zou het makkelijk zijn om alles met sudo-rechten (become: true) uit te voeren, maar dat is geen veilige optie.
Er twee mogelijkheden om dit aan te pakken.

  • Ofwel voer je volgende commando's (enkel waar dat nodig is) uit in naam van de user "www-data". Deze heeft immers voldoende rechten op de mappenstructuur, ook zonder sudo.
  • Ofwel voer je de commando's uit met de huidige ansible-gebruiker (ubuntu), maar dan moet deze lid gemaakt worden van de groep "www-data" om op die manier ook voldoende rechten te krijgen in de mappenstructuur.

Deze laatste optie lijkt makkelijk te realiseren door gebruik te maken van de module "ansible.builtin.user" om de user "ubuntu" lid te maken van de groep "www-data". Er is echter een klein angeltje... Indien het gaat om de gebruiker waarmee je de ansible-connectie maakt, dan wordt dit nieuwe groepslidmaatschap niet onmiddellijk toegepast. Dat zou enkel bij een volgende "run" van je ansible-playbook toegepast worden. En dat is natuurlijk te laat als je onmiddellijk daarna deze rechten nodig hebt. Hiervoor bestaat een kleine work-around, waarmee de connectie met de doelhost heel even onderbroken wordt zodat Ansible een nieuwe connectie moet opzetten en je lidmaatschap onmiddellijk in de nieuwe shell aanwezig is.

  - name: Kill SSH #https://github.com/ansible/ansible-modules-core/issues/921#issuecomment-220513111
shell: sleep 1; pkill -u {{ ansible_ssh_user }} sshd
async: 3
poll: 2
Python Virtual Environment installeren en starten

Een applicatie zoals Django is afhankelijk van een pak python modules. Deze moeten uiteraard allemaal geïnstalleerd worden vooraleer de applicatie kan starten. Volgens Python best practices kan elk project best in zijn eigen geïsoleerde omgeving functioneren. De geïnstalleerde packages werken dan enkel binnen deze virtual environment en kunnen geen conflicten veroorzaken met andere versies en andere applicaties.

  • installeer eerst de package in ubuntu die Python Virtual Environments kan creëren en gebruiken (python3-virtualenv)

  • Gebruik de pip module in ansible om een nieuwe virtual environment, met de naam .venv te creëren (in de root van je Django app-folder) en alle nodige python modules te installeren. Alle modules die nodig zijn voor dit project staan opgelijst in het bestand requirements.txt en kan op die manier ook onmiddellijk meegegeven worden met de pip-module.

  • In de volgende stap zullen we de aangemaakte virtual environment opstarten om daar enkele commando, die typisch bij ons django python-project horen, uit te voeren.
    Maak gebruik van de opgegeven template die je opslaat (bv. in de .venv-map) als script venv_activate.sh. Zorg dat de juiste rechten ingesteld staan zodat dit bestand uitvoerbaar is (bv mode 755).

  • Als laatste stap in deze rol mag het script venv_activate.sh uitgevoerd worden. (Het is mogelijk dat je "bash" voorafgaand aan je script moet gebruiken om het script te kunnen laten uitvoeren)

note

In dit script worden enkele zaken die typisch met het opslaan van data (in databanken) te maken hebben, uitgevoerd.
Een eerste commando voert makemigrations uit. Dit zal op basis van de data-modellen die gemaakt werden in het project (models.py), migration-files klaarzetten. Deze beschrijven oa. hoe het databank-schema er moet uitzien en welke wijzigingen er eventueel aan de databank moeten gebeuren. Een tweede commando, migrate, zal effectief deze wijzigingen gaan doorvoeren op de database (momenteel sqlite) en dus de nodige tabellen etc, implementeren.

controle

Op dit moment zou je eigenlijk al de geïnstalleerde versie van het Django-project lokaal moeten kunnen testen.
Zoals eerder verteld is er een "Develompent server" aanwezig in het Django-framework. Deze kan je activeren door, in de virtual environment, het volgende command uit te voeren:

source {{ je folder naar het project }}/iac_app/.venv/bin/activate
python3 ./manage.py runserver 0.0.0.0:8000

Het zou dan al moeten mogelijk zijn om te surfen naar je doelhosts via het ip-adres en poort 8000. Opgelet: vergeet deze development server niet terug uit te schakelen vooraleer verder te werken.

Stap 2 - Django Database van Sqlite naar MySql / Gunicorn ipv Development-server

Op dit moment maakt ons project nog gebruik van de ingebouwde Sqlite databank en dat op elke webnode apart.
De bedoeling is dat we uiteraard gebruik gaan maken van een gemeenschappelijke database, in dit geval onze MySql-database op host 3.

In de tweede stap gaan we de development-server vervangen door een echte Python WSGI server => Gunicorn.

Voorzie onderstaande dus in een nieuwe rol met de naam django_db.

Sqllite naar MySql
  • Django zal gebruik maken van de mysqlclient python package om zijn connectie met MySql op te zetten.
    Voorzie dus een task die de modules "mysqlclient" én "gunicorn" (straks nodig) via pip zal installeren.
    Maak in de module ook zeker gebruik van je virtual environment.

    tip

    Om de mysqlclient package te kunnen installeren zijn enkele andere libraries (dependencies) nodig. Deze zal je eerst via de gewone Linux package installer moeten voorzien.
    Zie https://pypi.org/project/mysqlclient/, sectie "Linux".

Nu moeten de database-connection settings aangepast worden van sqlite naar MySql. Django werd momenteel zo geconfigureerd dat de database-settings ingelezen worden via het bestand db.py in de map "PyEditorial".

  • Maak gebruik van de opgegeven template om het bestaande bestand db.py te overschrijven met deze nieuwe settings. Volgens de best practices komen deze settings uit een vault.
Gunicorn en Migrations

Straks gaan we gunicorn uitvoeren. Deze heeft een aantal parameters nodig.

  • Maak gebruik van de opgegeven template en plaats deze in een map en bestandsnaam: conf/gunicorn_config.py in de root van je project.
note

We zouden er voor kunnen kiezen om gunicorn als een systemd-service te laten draaien. Maar om de opgave niet te complex of uitgebreid te maken kiezen we voor de gewone opstart-methode, via de Python virtual environment.

Nu deze settings aangepast zijn moet het volledige databank-schema nog geïmplementeerd worden in de nieuwe databank, MySql. In de "note" van stap werd reeds aangehaald hoe dit met "makemigrations" en "migrate" doorgevoerd kan worden. Aangezien er in deze situatie gebruik gemaakt zal worden van gunicorn moet ons script van daarnet een beetje gewijzigd worden.

  • Maak gebruik van de opgegeven template om een aangepast script op de doelhost te plaatsen (bv. in de .venv-map). Let opnieuw op de juiste permissies zodat het script venv_activate_db_gunicorn.sh kan uitgevoerd worden.

  • Als laatste stap in deze rol mag het script venv_activate_db_gunicorn.sh uitgevoerd worden.
    (Het is mogelijk dat je "bash" voorafgaand aan je script moet gebruiken om het script te kunnen laten uitvoeren)

controle

Als controle zou je nu in principe rechtstreeks moeten kunnen connecteren met je webnodes op poort 8000. Alle sequests worden dan afgehandeld door gunicorn en ... je data zou moeten afkomstig zijn van je MySql database op host3.

Stap 3 - Nginx aanpassen

Het is niet de bedoeling dat gunicorn nu alle requests op zich neemt. Zoals eerder aangegeven kunnen bestanden die typisch statisch zijn (images, css, js, ...) beter door een gewone webserver afgehandeld worden.

In een laatste rol met de naam django_nginx passen we de configuratie aan van de nginx server zodat requests, komende van de loadbalancer vanop poort 80, opgesplitst worden.
Requests voor het URI-path /media en /static worden door nginx zelf afgehandeld. Alle andere requests worden door gunicorn afgehandeld en zullen dus door nginx, via een proxy_pass doorgestuurd worden.

  • Maak gebruik van de opgegeven template om een aangepaste nginx-configuratie in te stellen.
controle

Als alles goed verlopen is zouden requests die naar de loadbalancer gestuurd worden nu netjes naar de individuele nodes moeten doorgestuurd worden. Op hun beurt zou elke webserver nu de statische content zelf moeten afhandelen en de andere requests door gunicorn.

In de headers van de webpagina's die je opvraagt zou je ook duidelijk moeten zien van welke webserver de responses afkomstig zijn.