Nu cu mult timp în urmă a trebuit să scriu mai multe manuale Ansible pentru a pregăti serverul pentru implementarea unei aplicații Rails. Și, surprinzător, nu am găsit un manual simplu pas cu pas. Nu am vrut să copiez cartea de joc a altcuiva fără să înțeleg ce se întâmplă și, în cele din urmă, a trebuit să citesc documentația, adunând totul eu. Poate pot ajuta pe cineva să accelereze acest proces cu ajutorul acestui articol.
Primul lucru de înțeles este că ansible vă oferă o interfață convenabilă pentru a efectua o listă predefinită de acțiuni pe un(e) server(e) la distanță prin SSH. Nu există nicio magie aici, nu puteți instala un plugin și nu puteți obține o implementare zero a aplicației dvs. cu docker, monitorizare și alte bunătăți din cutie. Pentru a scrie un manual, trebuie să știi exact ce vrei să faci și cum să o faci. De aceea, nu sunt mulțumit de manualele gata făcute din GitHub sau de articole precum: „Copiați și rulați, va funcționa”.
De ce avem nevoie?
După cum am spus deja, pentru a scrie un manual, trebuie să știi ce vrei să faci și cum să o faci. Să decidem de ce avem nevoie. Pentru o aplicație Rails vom avea nevoie de mai multe pachete de sistem: nginx, postgresql (redis, etc). În plus, avem nevoie de o versiune specifică a rubinului. Cel mai bine este să-l instalați prin rbenv (rvm, asdf...). Rularea tuturor acestor lucruri ca utilizator root este întotdeauna o idee proastă, așa că trebuie să creați un utilizator separat și să îi configurați drepturile. După aceasta, trebuie să încărcați codul nostru pe server, să copiați configurațiile pentru nginx, postgres etc. și să porniți toate aceste servicii.
Ca urmare, succesiunea acțiunilor este următoarea:
- Conectați-vă ca root
- instalați pachete de sistem
- creați un utilizator nou, configurați drepturile, cheia ssh
- configurați pachetele de sistem (nginx etc) și rulați-le
- Creăm un utilizator în baza de date (puteți crea imediat o bază de date)
- Conectați-vă ca utilizator nou
- Instalați rbenv și ruby
- Instalarea bundler-ului
- Încărcarea codului aplicației
- Lansarea serverului Puma
Mai mult, ultimele etape pot fi efectuate folosind capistrano, cel puțin din cutie poate copia codul în directoare de lansare, poate comuta ediția cu un link simbolic la implementarea cu succes, poate copia configurațiile dintr-un director partajat, repornește puma etc. Toate acestea se pot face folosind Ansible, dar de ce?
Structura fișierului
Ansible are strict
Caiet de joc simplu
Playbook este un fișier yml care, folosind o sintaxă specială, descrie ce ar trebui să facă Ansible și cum. Să creăm primul manual care nu face nimic:
---
- name: Simple playbook
hosts: all
Aici spunem pur și simplu că se numește playbook-ul nostru Simple Playbook
și că conținutul său ar trebui să fie executat pentru toate gazdele. Îl putem salva în directorul /ansible cu numele playbook.yml
si incearca sa alergi:
ansible-playbook ./playbook.yml
PLAY [Simple Playbook] ************************************************************************************************************************************
skipping: no hosts matched
Ansible spune că nu cunoaște nicio gazdă care se potrivește cu toate listele. Ele trebuie listate într-un special
Să-l creăm în același director ansible:
123.123.123.123
Acesta este modul în care pur și simplu specificăm gazda (în mod ideal gazda VPS-ului nostru pentru testare, sau puteți înregistra localhost) și o salvați sub numele inventory
.
Puteți încerca să rulați ansible cu un fișier de inventar:
ansible-playbook ./playbook.yml -i inventory
PLAY [Simple Playbook] ************************************************************************************************************************************
TASK [Gathering Facts] ************************************************************************************************************************************
PLAY RECAP ************************************************************************************************************************************
Dacă aveți acces ssh la gazda specificată, atunci ansible se va conecta și va colecta informații despre sistemul de la distanță. (implicit TASK [Gathering Facts]) după care va da un scurt raport asupra execuției (PLAY RECAP).
În mod implicit, conexiunea folosește numele de utilizator sub care sunteți conectat la sistem. Cel mai probabil nu va fi pe gazdă. În fișierul playbook, puteți specifica ce utilizator să utilizați pentru a vă conecta folosind directiva remote_user. De asemenea, informațiile despre un sistem de la distanță pot fi adesea inutile pentru dvs. și nu ar trebui să pierdeți timpul colectându-le. Această sarcină poate fi, de asemenea, dezactivată:
---
- name: Simple playbook
hosts: all
remote_user: root
become: true
gather_facts: no
Încercați să rulați din nou playbook-ul și asigurați-vă că conexiunea funcționează. (Dacă ați specificat utilizatorul rădăcină, atunci trebuie să specificați și directiva become: true pentru a obține drepturi ridicate. După cum este scris în documentație: become set to ‘true’/’yes’ to activate privilege escalation.
deși nu este în întregime clar de ce).
Poate că veți primi o eroare cauzată de faptul că ansible nu poate determina interpretul Python, apoi îl puteți specifica manual:
ansible_python_interpreter: /usr/bin/python3
Puteți afla unde aveți python cu comanda whereis python
.
Instalarea pachetelor de sistem
Distribuția standard a Ansible include multe module pentru lucrul cu diverse pachete de sistem, astfel încât nu trebuie să scriem scripturi bash din niciun motiv. Acum avem nevoie de unul dintre aceste module pentru a actualiza sistemul și a instala pachetele de sistem. Am Ubuntu Linux pe VPS-ul meu, așa că pentru a instala pachetele pe care le folosesc apt-get
и
Să completăm manualul nostru cu primele sarcini:
---
- name: Simple playbook
hosts: all
remote_user: root
become: true
gather_facts: no
tasks:
- name: Update system
apt: update_cache=yes
- name: Install system dependencies
apt:
name: git,nginx,redis,postgresql,postgresql-contrib
state: present
Sarcina este exact sarcina pe care Ansible o va realiza pe serverele de la distanță. Dăm sarcinii un nume, astfel încât să putem urmări execuția acesteia în jurnal. Și descriem, folosind sintaxa unui anumit modul, ceea ce trebuie să facă. În acest caz apt: update_cache=yes
- spune să actualizați pachetele de sistem folosind modulul apt. A doua comandă este puțin mai complicată. Trecem o listă de pachete la modulul apt și spunem că sunt state
ar trebui să devină present
, adică spunem să instalăm aceste pachete. Într-un mod similar, le putem spune să le ștergă sau să le actualizeze prin simpla schimbare state
. Vă rugăm să rețineți că pentru ca șinele să funcționeze cu postgresql avem nevoie de pachetul postgresql-contrib, pe care îl instalăm acum. Din nou, trebuie să știți și să faceți acest lucru; ansible singur nu va face acest lucru.
Încercați să rulați din nou playbook-ul și verificați dacă pachetele sunt instalate.
Crearea de noi utilizatori.
Pentru a lucra cu utilizatorii, Ansible are și un modul - user. Să mai adăugăm o sarcină (am ascuns părțile deja cunoscute ale caietului de joc în spatele comentariilor pentru a nu-l copia complet de fiecare dată):
---
- name: Simple playbook
# ...
tasks:
# ...
- name: Add a new user
user:
name: my_user
shell: /bin/bash
password: "{{ 123qweasd | password_hash('sha512') }}"
Creăm un utilizator nou, setăm un program și o parolă pentru acesta. Și atunci ne confruntăm cu mai multe probleme. Ce se întâmplă dacă numele de utilizator trebuie să fie diferite pentru diferite gazde? Și stocarea parolei în text clar în manualul de joc este o idee foarte proastă. Pentru început, să punem numele de utilizator și parola în variabile, iar spre sfârșitul articolului voi arăta cum să criptăm parola.
---
- name: Simple playbook
# ...
tasks:
# ...
- name: Add a new user
user:
name: "{{ user }}"
shell: /bin/bash
password: "{{ user_password | password_hash('sha512') }}"
Variabilele sunt stabilite în manuale folosind acolade duble.
Vom indica valorile variabilelor în fișierul de inventar:
123.123.123.123
[all:vars]
user=my_user
user_password=123qweasd
Vă rugăm să rețineți directiva [all:vars]
- se spune că următorul bloc de text este variabile (vars) și sunt aplicabile tuturor gazdelor (toate).
Designul este de asemenea interesant "{{ user_password | password_hash('sha512') }}"
. Chestia este că ansible nu instalează utilizatorul prin user_add
cum ai face-o manual. Și salvează toate datele direct, motiv pentru care trebuie să convertim și parola într-un hash în avans, ceea ce face această comandă.
Să adăugăm utilizatorul nostru la grupul sudo. Cu toate acestea, înainte de aceasta, trebuie să ne asigurăm că un astfel de grup există, deoarece nimeni nu va face asta pentru noi:
---
- name: Simple playbook
# ...
tasks:
# ...
- name: Ensure a 'sudo' group
group:
name: sudo
state: present
- name: Add a new user
user:
name: "{{ user }}"
shell: /bin/bash
password: "{{ user_password | password_hash('sha512') }}"
groups: "sudo"
Totul este destul de simplu, avem și un modul de grup pentru crearea de grupuri, cu o sintaxă foarte asemănătoare cu apt. Atunci este suficient să înregistrați acest grup la utilizator (groups: "sudo"
).
De asemenea, este util să adăugați o cheie ssh acestui utilizator, astfel încât să ne putem autentifica folosind-o fără parolă:
---
- name: Simple playbook
# ...
tasks:
# ...
- name: Ensure a 'sudo' group
group:
name: sudo
state: present
- name: Add a new user
user:
name: "{{ user }}"
shell: /bin/bash
password: "{{ user_password | password_hash('sha512') }}"
groups: "sudo"
- name: Deploy SSH Key
authorized_key:
user: "{{ user }}"
key: "{{ lookup('file', '~/.ssh/id_rsa.pub') }}"
state: present
În acest caz, designul este interesant "{{ lookup('file', '~/.ssh/id_rsa.pub') }}"
— copiază conținutul fișierului id_rsa.pub (numele tău poate fi diferit), adică partea publică a cheii ssh și o încarcă în lista de chei autorizate pentru utilizator de pe server.
rol
Toate cele trei sarcini pentru crearea utilizării pot fi clasificate cu ușurință într-un singur grup de sarcini și ar fi o idee bună să stocați acest grup separat de manualul principal, astfel încât să nu devină prea mare. În acest scop, Ansible are
Conform structurii de fișiere indicate la început, rolurile trebuie plasate într-un director separat de roluri, pentru fiecare rol există un director separat cu același nume, în interiorul directorului task-uri, fișiere, șabloane etc.
Să creăm o structură de fișiere: ./ansible/roles/user/tasks/main.yml
(principal este fișierul principal care va fi încărcat și executat atunci când un rol este conectat la playbook; alte fișiere de rol pot fi conectate la acesta). Acum puteți transfera toate sarcinile legate de utilizator în acest fișier:
# Create user and add him to groups
- name: Ensure a 'sudo' group
group:
name: sudo
state: present
- name: Add a new user
user:
name: "{{ user }}"
shell: /bin/bash
password: "{{ user_password | password_hash('sha512') }}"
groups: "sudo"
- name: Deploy SSH Key
authorized_key:
user: "{{ user }}"
key: "{{ lookup('file', '~/.ssh/id_rsa.pub') }}"
state: present
În manualul principal, trebuie să specificați pentru a utiliza rolul de utilizator:
---
- name: Simple playbook
hosts: all
remote_user: root
gather_facts: no
tasks:
- name: Update system
apt: update_cache=yes
- name: Install system dependencies
apt:
name: git,nginx,redis,postgresql,postgresql-contrib
state: present
roles:
- user
De asemenea, poate avea sens să actualizați sistemul înainte de toate celelalte sarcini; pentru a face acest lucru, puteți redenumi blocul tasks
în care sunt definite în pre_tasks
.
Configurarea nginx
Ar trebui să avem deja instalat Nginx; trebuie să îl configuram și să îl rulăm. Să o facem imediat în rol. Să creăm o structură de fișiere:
- ansible
- roles
- nginx
- files
- tasks
- main.yml
- templates
Acum avem nevoie de fișiere și șabloane. Diferența dintre ele este că ansible copiază fișierele direct, așa cum sunt. Și șabloanele trebuie să aibă extensia j2 și pot folosi valori variabile folosind aceleași acolade duble.
Să activăm nginx main.yml
fişier. Pentru aceasta avem un modul systemd:
# Copy nginx configs and start it
- name: enable service nginx and start
systemd:
name: nginx
state: started
enabled: yes
Aici nu spunem doar că nginx trebuie pornit (adică îl lansăm), dar spunem imediat că trebuie activat.
Acum să copiem fișierele de configurare:
# Copy nginx configs and start it
- name: enable service nginx and start
systemd:
name: nginx
state: started
enabled: yes
- name: Copy the nginx.conf
copy:
src: nginx.conf
dest: /etc/nginx/nginx.conf
owner: root
group: root
mode: '0644'
backup: yes
- name: Copy template my_app.conf
template:
src: my_app_conf.j2
dest: /etc/nginx/sites-available/my_app.conf
owner: root
group: root
mode: '0644'
Creăm fișierul principal de configurare nginx ( îl puteți lua direct de pe server sau îl puteți scrie singur). Și, de asemenea, fișierul de configurare pentru aplicația noastră în directorul site_available (acest lucru nu este necesar, dar util). În primul caz, folosim modulul de copiere pentru a copia fișiere (fișierul trebuie să fie în /ansible/roles/nginx/files/nginx.conf
). În al doilea, copiem șablonul, înlocuind valorile variabilelor. Șablonul ar trebui să fie în /ansible/roles/nginx/templates/my_app.j2
). Și ar putea arăta cam așa:
upstream {{ app_name }} {
server unix:{{ app_path }}/shared/tmp/sockets/puma.sock;
}
server {
listen 80;
server_name {{ server_name }} {{ inventory_hostname }};
root {{ app_path }}/current/public;
try_files $uri/index.html $uri.html $uri @{{ app_name }};
....
}
Acordați atenție inserțiilor {{ app_name }}
, {{ app_path }}
, {{ server_name }}
, {{ inventory_hostname }}
— acestea sunt toate variabilele ale căror valori Ansible le va înlocui în șablon înainte de copiere. Acest lucru este util dacă utilizați un manual pentru diferite grupuri de gazde. De exemplu, putem adăuga fișierul nostru de inventar:
[production]
123.123.123.123
[staging]
231.231.231.231
[all:vars]
user=my_user
user_password=123qweasd
[production:vars]
server_name=production
app_path=/home/www/my_app
app_name=my_app
[staging:vars]
server_name=staging
app_path=/home/www/my_stage
app_name=my_stage_app
Dacă lansăm acum playbook-ul nostru, acesta va îndeplini sarcinile specificate pentru ambele gazde. Dar, în același timp, pentru o gazdă de staging, variabilele vor fi diferite de cele de producție, și nu numai în roluri și playbook-uri, ci și în configurațiile nginx. {{ inventory_hostname }}
nu trebuie specificate în dosarul de inventar - aceasta
Dacă doriți să aveți un fișier de inventar pentru mai multe gazde, dar să rulați doar pentru un grup, acest lucru se poate face cu următoarea comandă:
ansible-playbook -i inventory ./playbook.yml -l "staging"
O altă opțiune este să aveți fișiere de inventar separate pentru diferite grupuri. Sau puteți combina cele două abordări dacă aveți multe gazde diferite.
Să revenim la configurarea nginx. După ce am copiat fișierele de configurare, trebuie să creăm un link simbolic în sitetest_enabled către my_app.conf din sites_available. Și reporniți nginx.
... # old code in mail.yml
- name: Create symlink to sites-enabled
file:
src: /etc/nginx/sites-available/my_app.conf
dest: /etc/nginx/sites-enabled/my_app.conf
state: link
- name: restart nginx
service:
name: nginx
state: restarted
Totul este simplu aici - din nou module ansible cu o sintaxă destul de standard. Dar există un punct. Nu are rost să reporniți nginx de fiecare dată. Ați observat că nu scriem comenzi de genul: „fă asta așa”, sintaxa arată mai mult ca „acest lucru ar trebui să aibă această stare”. Și cel mai adesea așa funcționează ansible. Dacă grupul există deja sau pachetul de sistem este deja instalat, atunci ansible va verifica acest lucru și va omite sarcina. De asemenea, fișierele nu vor fi copiate dacă se potrivesc complet cu ceea ce este deja pe server. Putem profita de acest lucru și repornim nginx numai dacă fișierele de configurare au fost modificate. Există o directivă de registru pentru aceasta:
# Copy nginx configs and start it
- name: enable service nginx and start
systemd:
name: nginx
state: started
enabled: yes
- name: Copy the nginx.conf
copy:
src: nginx.conf
dest: /etc/nginx/nginx.conf
owner: root
group: root
mode: '0644'
backup: yes
register: restart_nginx
- name: Copy template my_app.conf
template:
src: my_app_conf.j2
dest: /etc/nginx/sites-available/my_app.conf
owner: root
group: root
mode: '0644'
register: restart_nginx
- name: Create symlink to sites-enabled
file:
src: /etc/nginx/sites-available/my_app.conf
dest: /etc/nginx/sites-enabled/my_app.conf
state: link
- name: restart nginx
service:
name: nginx
state: restarted
when: restart_nginx.changed
Dacă unul dintre fișierele de configurare se modifică, se va face o copie și variabila va fi înregistrată restart_nginx
. Și numai dacă această variabilă a fost înregistrată, serviciul va fi repornit.
Și, desigur, trebuie să adăugați rolul nginx în manualul principal.
Configurarea postgresql
Trebuie să activăm postgresql folosind systemd în același mod ca și cu nginx și, de asemenea, să creăm un utilizator pe care îl vom folosi pentru a accesa baza de date și baza de date în sine.
Să creăm un rol /ansible/roles/postgresql/tasks/main.yml
:
# Create user in postgresql
- name: enable postgresql and start
systemd:
name: postgresql
state: started
enabled: yes
- name: Create database user
become_user: postgres
postgresql_user:
name: "{{ db_user }}"
password: "{{ db_password }}"
role_attr_flags: SUPERUSER
- name: Create database
become_user: postgres
postgresql_db:
name: "{{ db_name }}"
encoding: UTF-8
owner: "{{ db_user }}"
Nu voi descrie cum să adăugați variabile la inventar, acest lucru a fost deja făcut de multe ori, precum și sintaxa modulelor postgresql_db și postgresql_user. Mai multe informații găsiți în documentație. Cea mai interesantă directivă de aici este become_user: postgres
. Cert este că, în mod implicit, doar utilizatorul postgres are acces la baza de date postgresql și doar local. Această directivă ne permite să executăm comenzi în numele acestui utilizator (dacă avem acces, desigur).
De asemenea, poate fi necesar să adăugați o linie la pg_hba.conf pentru a permite accesul unui nou utilizator la baza de date. Acest lucru se poate face în același mod în care am schimbat configurația nginx.
Și, desigur, trebuie să adăugați rolul postgresql în manualul principal.
Instalarea ruby prin rbenv
Ansible nu are module pentru lucrul cu rbenv, dar este instalat prin clonarea unui depozit git. Prin urmare, această problemă devine cea mai non-standard. Să creăm un rol pentru ea /ansible/roles/ruby_rbenv/main.yml
și să începem să o completăm:
# Install rbenv and ruby
- name: Install rbenv
become_user: "{{ user }}"
git: repo=https://github.com/rbenv/rbenv.git dest=~/.rbenv
Folosim din nou directiva become_user pentru a lucra sub utilizatorul pe care l-am creat în aceste scopuri. Deoarece rbenv este instalat în directorul său principal și nu global. Și folosim și modulul git pentru a clona depozitul, specificând repo și dest.
Apoi, trebuie să înregistrăm rbenv init în bashrc și să adăugăm rbenv la PATH acolo. Pentru aceasta avem modulul lineinfile:
- name: Add rbenv to PATH
become_user: "{{ user }}"
lineinfile:
path: ~/.bashrc
state: present
line: 'export PATH="${HOME}/.rbenv/bin:${PATH}"'
- name: Add rbenv init to bashrc
become_user: "{{ user }}"
lineinfile:
path: ~/.bashrc
state: present
line: 'eval "$(rbenv init -)"'
Apoi trebuie să instalați ruby_build:
- name: Install ruby-build
become_user: "{{ user }}"
git: repo=https://github.com/rbenv/ruby-build.git dest=~/.rbenv/plugins/ruby-build
Și în cele din urmă instalați ruby. Acest lucru se face prin rbenv, adică pur și simplu cu comanda bash:
- name: Install ruby
become_user: "{{ user }}"
shell: |
export PATH="${HOME}/.rbenv/bin:${PATH}"
eval "$(rbenv init -)"
rbenv install {{ ruby_version }}
args:
executable: /bin/bash
Spunem ce comandă să executăm și cu ce. Totuși, aici întâlnim faptul că ansible nu rulează codul conținut în bashrc înainte de a rula comenzile. Aceasta înseamnă că rbenv va trebui definit direct în același script.
Următoarea problemă se datorează faptului că comanda shell nu are nicio stare din punct de vedere ansible. Adică, nu va exista o verificare automată dacă această versiune de ruby este instalată sau nu. Putem face asta singuri:
- name: Install ruby
become_user: "{{ user }}"
shell: |
export PATH="${HOME}/.rbenv/bin:${PATH}"
eval "$(rbenv init -)"
if ! rbenv versions | grep -q {{ ruby_version }}
then rbenv install {{ ruby_version }} && rbenv global {{ ruby_version }}
fi
args:
executable: /bin/bash
Tot ce rămâne este să instalezi bundler:
- name: Install bundler
become_user: "{{ user }}"
shell: |
export PATH="${HOME}/.rbenv/bin:${PATH}"
eval "$(rbenv init -)"
gem install bundler
Și din nou, adăugați rolul nostru ruby_rbenv în manualul principal.
Fișiere partajate.
În general, configurarea ar putea fi finalizată aici. În continuare, tot ce rămâne este să rulați capistrano și acesta va copia în sine codul, va crea directoarele necesare și va lansa aplicația (dacă totul este configurat corect). Cu toate acestea, capistrano necesită adesea fișiere de configurare suplimentare, cum ar fi database.yml
sau .env
Ele pot fi copiate la fel ca fișierele și șabloanele pentru nginx. Există o singură subtilitate. Înainte de a copia fișierele, trebuie să creați o structură de directoare pentru ele, ceva de genul acesta:
# Copy shared files for deploy
- name: Ensure shared dir
become_user: "{{ user }}"
file:
path: "{{ app_path }}/shared/config"
state: directory
specificăm un singur director și ansible va crea automat unul părinte dacă este necesar.
Ansible Vault
Am dat deja peste faptul că variabilele pot conține date secrete, cum ar fi parola utilizatorului. Dacă ați creat .env
dosar pentru cerere și database.yml
atunci trebuie să existe și mai multe astfel de date critice. Ar fi bine să le ascundem de privirile indiscrete. În acest scop este folosit
Să creăm un fișier pentru variabile /ansible/vars/all.yml
(aici puteți crea fișiere diferite pentru diferite grupuri de gazde, la fel ca în fișierul de inventar: production.yml, staging.yml, etc).
Toate variabilele care trebuie criptate trebuie transferate în acest fișier folosind sintaxa standard yml:
# System vars
user_password: 123qweasd
db_password: 123qweasd
# ENV vars
aws_access_key_id: xxxxx
aws_secret_access_key: xxxxxx
aws_bucket: bucket_name
rails_secret_key_base: very_secret_key_base
După care acest fișier poate fi criptat cu comanda:
ansible-vault encrypt ./vars/all.yml
Desigur, atunci când criptați, va trebui să setați o parolă pentru decriptare. Puteți vedea ce va fi în interiorul fișierului după apelarea acestei comenzi.
Cu ajutorul ansible-vault decrypt
fișierul poate fi decriptat, modificat și apoi criptat din nou.
Nu trebuie să decriptați fișierul pentru a funcționa. Îl stocați criptat și rulați playbook-ul cu argumentul --ask-vault-pass
. Ansible va cere parola, va prelua variabilele și va executa sarcinile. Toate datele vor rămâne criptate.
Comanda completă pentru mai multe grupuri de gazde și seif ansible va arăta cam așa:
ansible-playbook -i inventory ./playbook.yml -l "staging" --ask-vault-pass
Dar nu vă voi oferi textul complet al manualelor și al rolurilor, scrieți-l singur. Pentru că ansible este așa - dacă nu înțelegi ce trebuie făcut, atunci nu o va face pentru tine.
Sursa: www.habr.com