Non molto tempo fa, ho dovuto scrivere alcuni playbook Ansible per preparare un server al deployment di un'applicazione Rails. E, con sorpresa, non ho trovato una guida passo passo semplice. Non volevo copiare il playbook di qualcun altro senza capire cosa stesse succedendo, quindi ho dovuto consultare la documentazione, raccogliendo tutto da solo. Forse posso aiutare qualcuno a velocizzare questo processo con questo articolo.
Per prima cosa, è importante capire che Ansible ti fornisce un'interfaccia comoda per eseguire un elenco di azioni predefinite su un server (o più server) remoto tramite SSH. Qui non c'è alcuna magia, non puoi installare un plugin e ottenere out-of-the-box un deployment senza downtime della tua applicazione con Docker, monitoraggio e altre funzionalità. Per scrivere un playbook, devi sapere esattamente cosa vuoi fare e come farlo. Pertanto, non sono soddisfatto dei playbook pronti su GitHub, o di articoli del tipo: “Copia e incolla, funzionerà”.
Cosa ci serve?
Come ho già detto, per scrivere un playbook devi sapere cosa vuoi fare e come farlo. Determiniamo quindi cosa ci serve. Per un'applicazione Rails avremo bisogno di alcuni pacchetti di sistema: nginx, postgresql (redis, ecc.). Inoltre, abbiamo bisogno di una versione specifica di Ruby. È meglio installarlo tramite rbenv (rvm, asdf…). Eseguire tutto come utente root è sempre una cattiva idea, quindi è necessario creare un utente separato e configurargli i permessi. Dopo di che, dobbiamo caricare il nostro codice sul server, copiare i file di configurazione per nginx, postgres, ecc. e avviare tutti questi servizi.
Di conseguenza, la sequenza di azioni è la seguente:
- Accediamo come root
- installiamo i pacchetti di sistema
- creiamo un nuovo utente, configuriamo i permessi e la chiave ssh
- configuriamo i pacchetti di sistema (nginx, ecc.) e li avviamo
- creiamo un utente nel database (possibilmente creiamo anche il database)
- Accediamo come nuovo utente
- installiamo rbenv e Ruby
- installiamo Bundler
- carichiamo il codice dell'applicazione
- avviamo il server Puma
Questi ultimi passaggi possono essere effettuati anche tramite Capistrano, che almeno di default sa copiare il codice nelle directory di rilascio, cambiare il rilascio tramite symlink in caso di deployment riuscito, copiare i file di configurazione dalla directory condivisa, riavviare Puma, ecc. Tutto questo può essere fatto anche con Ansible, ma perché?
Struttura dei file
Ansible ha una per tutti i suoi file, quindi è meglio tenere tutto questo in una directory separata. Non importa se sarà all'interno dell'applicazione Rails o separata. Puoi conservare i file in un repository Git separato. Personalmente, mi è sembrato più comodo creare una directory ansible nella directory /config dell'applicazione Rails e conservare tutto in un unico repository.
Playbook Semplice
Un playbook è un file yml in cui viene descritto, tramite una sintassi speciale, cosa e come deve fare Ansible. Creiamo ora il nostro primo playbook, che non fa nulla:
---
- name: Playbook Semplice
hosts: allQui stiamo semplicemente dicendo che il nostro playbook si chiama Playbook Semplice e che il suo contenuto deve essere eseguito per tutti gli host. Possiamo salvarlo nella directory /ansible con il nome playbook.yml e provare a eseguirlo:
ansible-playbook ./playbook.yml
PLAY [Playbook Semplice] ************************************************************************************************************************************
skipping: no hosts matchedAnsible dice che non conosce gli host che corrispondono all'elenco all. Devono essere elencati in un file speciale .
Così semplicemente indichiamo l'host (ideale sarebbe l'host del proprio VPS per i test, oppure puoi inserire localhost) e lo salviamo con il nome
123.123.123.123Possiamo provare a eseguire Ansible con il file di inventario: inventory.
ansible-playbook ./playbook.yml -i inventory PLAY [Playbook Semplice] ************************************************************************************************************************************TASK [Raccolta Fatti] ************************************************************************************************************************************PLAY RECAP ************************************************************************************************************************************
Se hai accesso ssh all'host specificato, Ansible si collegherà e raccoglierà informazioni sul sistema remoto. (TASK predefinito [Raccolta Fatti]) dopo di che darà un breve rapporto sull'esecuzione (PLAY RECAP).Per impostazione predefinita, viene utilizzato il nome utente con cui sei connesso al sistema per la connessione. Sull'host, probabilmente, non ci sarà. Nel file del playbook puoi specificare quale utente utilizzare per la connessione tramite la direttiva remote_user. Inoltre, le informazioni sul sistema remoto possono spesso essere inutili e non vale la pena sprecare tempo per raccoglierle. Anche questa operazione può essere disabilitata:
--- - name: Playbook Semplice hosts: all remote_user: root become: true gather_facts: no
---
- name: Playbook semplice
hosts: tutti
remote_user: root
become: true
gather_facts: noRiprova a eseguire il playbook e assicurati che la connessione funzioni. (Se hai specificato l'utente root, devi anche impostare la direttiva become: true per ottenere i privilegi elevati. Come indicato nella documentazione: become impostato su 'true'/'yes' per attivare l'escalation dei privilegi. anche se non è del tutto chiaro perché).
Potresti ricevere un errore causato dal fatto che Ansible non riesce a determinare l'interprete Python, quindi puoi specificarlo manualmente:
ansible_python_interpreter: /usr/bin/python3 puoi scoprire dove si trova Python con il comando whereis python.
Instalazione dei pacchetti di sistema
La distribuzione standard di Ansible include numerosi moduli per lavorare con vari pacchetti di sistema, il che ci evita di dover scrivere script bash per ogni evenienza. Ora avremo bisogno di uno di questi moduli per aggiornare il sistema e installare pacchetti di sistema. Sul mio VPS è installato Ubuntu Linux, quindi utilizzo apt-get e . Se utilizzi un altro sistema operativo, potrebbe essere necessario un modulo diverso (ricorda che all'inizio ho detto che è necessario sapere in anticipo cosa e come fare). Tuttavia, la sintassi sarà probabilmente simile.
Aggiungiamo le prime attività al nostro playbook:
---
- name: Simple playbook
hosts: all
remote_user: root
become: true
gather_facts: no
tasks:
- name: Aggiorna il sistema
apt: update_cache=yes
- name: Installa dipendenze di sistema
apt:
name: git,nginx,redis,postgresql,postgresql-contrib
state: presentUn Task è esattamente l'attività che Ansible eseguirà sui server remoti. Diamo un nome all'attività per monitorarne l'esecuzione nel log. E descriviamo, usando la sintassi del modulo specifico, ciò che deve fare. In questo caso, apt: update_cache=yes significa aggiornare i pacchetti di sistema utilizzando il modulo apt. Il secondo comando è un po' più complesso. Stiamo passando al modulo apt un elenco di pacchetti e diciamo che devono essere state installati present, cioè diciamo di installare questi pacchetti. Allo stesso modo, possiamo dirgli di rimuoverli o aggiornarli, semplicemente cambiando state. Tieni presente che per far funzionare Rails con PostgreSQL abbiamo bisogno del pacchetto postgresql-contrib, che stiamo attualmente installando. Anche questo è qualcosa da sapere e fare; Ansible non lo farà da solo.
Riprova a eseguire il playbook e verifica che i pacchetti vengano installati.
Creazione di nuovi utenti.
Per gestire gli utenti, Ansible ha anche un modulo — user. Aggiungiamo un'altra attività (ho nascosto le parti già note del playbook con commenti per non doverlo copiare interamente ogni volta):
---
- name: Simple playbook
# ...
tasks:
# ...
- name: Aggiungi un nuovo utente
user:
name: my_user
shell: /bin/bash
password: "{{ 123qweasd | password_hash('sha512') }}"Stiamo creando un nuovo utente, impostando la sua shell e la sua password. E ci troviamo subito di fronte a diversi problemi. Cosa succede se i nomi degli utenti devono essere diversi per diversi host? Inoltre, memorizzare la password in chiaro nel playbook è un'idea molto cattiva. Cominciamo a spostare il nome utente e la password in variabili, e più avanti nell'articolo mostrerò come crittografare la password.
---
- name: Simple playbook
# ...
tasks:
# ...
- name: Aggiungi un nuovo utente
user:
name: "{{ user }}"
shell: /bin/bash
password: "{{ user_password | password_hash('sha512') }}"le variabili vengono impostate nei playbook utilizzando le doppie parentesi graffe.
Indicheremo i valori delle variabili nel file di inventory:
123.123.123.123
[all:vars]
user=my_user
user_password=123qweasdFai attenzione alla direttiva [all:vars] — indica che il blocco di testo successivo sono variabili (vars) e sono applicabili a tutti gli host (all).
È interessante anche la costruzione "{{ user_password | password_hash('sha512') }}".Il fatto è che Ansible non aggiunge l'utente tramite user_add come faresti manualmente. Salva direttamente tutti i dati, motivo per cui dobbiamo anche trasformare in hash la password in anticipo, come fa questo comando.
Aggiungiamo il nostro utente al gruppo sudo. Tuttavia, prima dobbiamo assicurarci che tale gruppo esista, perché nessuno lo farà per noi:
---
- name: Simple playbook
# ...
tasks:
# ...
- name: Assicurati che ci sia un gruppo 'sudo'
group:
name: sudo
state: present
- name: Aggiungi un nuovo utente
user:
name: "{{ user }}"
shell: /bin/bash
password: "{{ user_password | password_hash('sha512') }}"
groups: "sudo"È tutto piuttosto semplice, abbiamo anche un modulo group per creare gruppi, con una sintassi molto simile a quella di apt. Dopo di che basta associare questo gruppo all'utente (groups: "sudo").
È utile anche aggiungere a quest'utente una chiave SSH, in modo da poter accedere senza password:
---
- name: Simple playbook
# ...
tasks:
# ...
- name: Assicurati che ci sia un gruppo 'sudo'
group:
name: sudo
state: present
- name: Aggiungi un nuovo utente
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: presentIn questo caso è interessante la costruzione "{{ lookup('file', '~/.ssh/id_rsa.pub') }}" — copia il contenuto del file id_rsa.pub (il tuo nome potrebbe essere diverso), ovvero la parte pubblica della chiave ssh e lo carica nell'elenco delle chiavi autorizzate per l'utente sul server.
Ruoli
Tutte e tre le attività per la creazione possono essere facilmente raggruppate in un'unica categoria, e sarebbe utile mantenere questo gruppo separato dal playbook principale, per evitare che diventi troppo grande. A tal fine, in ansible esistono .
Secondo la struttura dei file indicata all'inizio, le role devono essere posizionate in una directory separata chiamata roles, per ogni role — una directory separata con lo stesso nome, all'interno delle directory tasks, files, templates, ecc.
Creiamo la struttura dei file: ./ansible/roles/user/tasks/main.yml (main è il file principale che verrà caricato ed eseguito quando si collega la role al playbook; in esso puoi includere altri file della role). Ora possiamo trasferire in questo file tutte le attività relative all'utente:
# 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: presentNel playbook principale è necessario specificare di utilizzare la role user:
---
- 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:
- userInoltre, potrebbe avere senso eseguire l'aggiornamento del sistema prima di tutte le altre attività, per fare ciò puoi rinominare il blocco tasks nel quale sono definiti pre_tasks.
Configurazione di nginx
Nginx dovrebbe essere già installato, è necessario configurarlo e avviarlo. Facciamolo subito nella role. Creiamo la struttura dei file:
- ansible
- roles
- nginx
- files
- tasks
- main.yml
- templatesOra avremo bisogno di file e template. La differenza tra i due è che Ansible copia i file direttamente, così come sono. I template devono avere l'estensione j2 e possono utilizzare i valori delle variabili tramite le stesse doppie parentesi graffe.
Includiamo nginx in main.yml file. A tal fine abbiamo il modulo systemd:
# Copy nginx configs and start it
- name: enable service nginx and start
systemd:
name: nginx
state: started
enabled: yesQui non solo indichiamo che nginx deve essere avviato (cioè lo stiamo avviando), ma diciamo subito che deve essere abilitato.
Ora copiamo i file di configurazione:
# 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'Stiamo creando il file di configurazione principale di nginx (puoi prenderlo direttamente dal server o scriverlo tu stesso). E anche il file di configurazione per la nostra applicazione nella directory sites_available (questo non è obbligatorio ma utile). Nel primo caso utilizziamo il modulo copy per copiare i file (il file deve trovarsi in /ansible/roles/nginx/files/nginx.conf). Nel secondo caso, copiamo il template, sostituendo i valori delle variabili. Il template deve trovarsi in /ansible/roles/nginx/templates/my_app.j2). Ed avere un aspetto simile a questo:
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 }};
....
}Fai attenzione alle inserzioni {{ app_name }}, {{ app_path }}, {{ server_name }}, {{ inventory_hostname }} — tutte queste sono variabili, i cui valori Ansible sostituirà nel template prima di copiarlo. Questo è utile se si utilizza il playbook per diversi gruppi di host. Ad esempio, possiamo completare il nostro file inventory:
[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_appSe ora eseguiremo il nostro playbook, eseguirà le attività indicate per entrambi gli host. Ma per l'host staging, le variabili saranno diverse da quelle di produzione, e non solo nelle role e nei playbook, ma anche nelle configurazioni di nginx. {{ inventory_hostname }} non è necessario specificarlo nel file inventory — è e lì viene memorizzato l'host per il quale il playbook viene eseguito in quel momento.
Se desideri avere un file inventory per più host ma eseguire solo per un gruppo, puoi farlo con il seguente comando:
ansible-playbook -i inventory ./playbook.yml -l "staging"un'altra opzione è avere file inventory separati per diversi gruppi. Oppure puoi combinare i due approcci se hai molti host diversi.
Ritorniamo alla configurazione di nginx. Dopo aver copiato i file di configurazione, dobbiamo creare un symlink in sites_enabled su my_app.conf dalla directory sites_available. E riavviare nginx.
... # codice vecchio 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: restartedQui è tutto semplice: di nuovo i moduli Ansible con una sintassi piuttosto standard. Ma c'è un aspetto da considerare. Riavviare Nginx ogni volta non ha senso. Avete notato che non scriviamo comandi come: «fai questo in questo modo», la sintassi appare piuttosto come «questo deve avere questo stato». E spesso è così che Ansible funziona. Se il gruppo esiste già, o se il pacchetto di sistema è già installato, Ansible lo verificherà e salterà il compito. Allo stesso modo, i file non saranno copiati se sono esattamente uguali a quelli già presenti sul server. Possiamo sfruttare questo e riavviare Nginx solo se i file di configurazione sono stati modificati. A questo scopo esiste la direttiva register:
# 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.changedSe uno dei file di configurazione viene modificato, verrà eseguita la copia e sarà registrata la variabile restart_nginx. E solo se questa variabile è stata registrata, verrà eseguito il riavvio del servizio.
E naturalmente, è necessario aggiungere il ruolo Nginx nel playbook principale.
Configurazione di PostgreSQL
Dobbiamo abilitare PostgreSQL utilizzando systemd proprio come abbiamo fatto con Nginx, e creare anche un utente che utilizzeremo per accedere al database e il database stesso.
Creiamo il ruolo /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 }}"Non entrerò nei dettagli su come aggiungere variabili nell'inventario, è stato già fatto molte volte, così come la sintassi per i moduli postgresql_db e postgresql_user. Maggiori dettagli si possono trovare nella documentazione. Qui la direttiva più interessante è become_user: postgres. Il fatto è che, per impostazione predefinita, solo l'utente postgres ha accesso al database PostgreSQL e solo localmente. Questa direttiva ci consente di eseguire comandi a nome di questo utente (se ovviamente abbiamo accesso).
Potrebbe essere necessario aggiungere una riga in pg_hba.conf per consentire al nuovo utente di accedere al database. Questo può essere fatto come abbiamo modificato la configurazione di Nginx.
E naturalmente bisogna aggiungere il ruolo PostgreSQL nel playbook principale.
Installazione di Ruby tramite rbenv
In Ansible non ci sono moduli per lavorare con rbenv, e viene installato clonando il repository git. Pertanto, questo compito diventa il più non standard. Creiamo un ruolo per questo /ansible/roles/ruby_rbenv/main.yml e iniziamo a compilarlo:
# Install rbenv and ruby
- name: Install rbenv
become_user: "{{ user }}"
git: repo=https://github.com/rbenv/rbenv.git dest=~/.rbenvUtilizziamo di nuovo la direttiva become_user per lavorare come l'utente che abbiamo creato per questi scopi. Poiché rbenv si installa nella sua home directory, e non globalmente. Utilizziamo anche il modulo git per clonare il repository, specificando repo e dest.
Poi dobbiamo aggiungere rbenv init in bashrc e aggiungere rbenv a PATH. Per questo abbiamo il modulo lineinfile:
- name: Aggiungi rbenv a PATH
become_user: "{{ user }}"
lineinfile:
path: ~/.bashrc
state: present
line: 'export PATH="${HOME}/.rbenv/bin:${PATH}"'
- name: Aggiungi rbenv init a bashrc
become_user: "{{ user }}"
lineinfile:
path: ~/.bashrc
state: present
line: 'eval "$(rbenv init -)"'Dopo di che dobbiamo installare ruby_build:
- name: Installa ruby-build
become_user: "{{ user }}"
git: repo=https://github.com/rbenv/ruby-build.git dest=~/.rbenv/plugins/ruby-buildE infine, installiamo Ruby. Questo si fa tramite rbenv, quindi semplicemente con un comando bash:
- name: Installa ruby
become_user: "{{ user }}"
shell: |
export PATH="${HOME}/.rbenv/bin:${PATH}"
eval "$(rbenv init -)"
rbenv install {{ ruby_version }}
args:
executable: /bin/bashDiciamo quale comando eseguire e con cosa. Tuttavia, ci imbatteremo nel fatto che Ansible non esegue il codice contenuto in bashrc prima di eseguire i comandi. Quindi, dobbiamo definire rbenv direttamente in questo stesso script.
Il problema successivo è che il comando shell non ha stato dal punto di vista di Ansible. Cioè, non ci sarà controllo automatico se questa versione di Ruby è installata o meno. Possiamo farlo noi stessi:
- name: Installa 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/bashE rimane da installare bundler:
- name: Installa bundler
become_user: "{{ user }}"
shell: |
export PATH="${HOME}/.rbenv/bin:${PATH}"
eval "$(rbenv init -)"
gem install bundlerE anche aggiungere il nostro ruolo ruby_rbenv nel playbook principale.
File condivisi.
In effetti, a questo punto si potrebbe terminare la configurazione. Resta solo da eseguire capistrano che copierà automaticamente il codice, creerà le directory necessarie e avvierà l'applicazione (se tutto è stato configurato correttamente). Tuttavia, spesso capistrano richiede file di configurazione aggiuntivi, come database.yml o è destinato a leggere le registrazioni dal file Possono essere copiati esattamente come file e modelli per Nginx. C'è solo una piccola complicazione. Prima di copiare i file, è necessario creare la struttura delle directory, simile a questa:
# Copy shared files for deploy
- name: Ensure shared dir
become_user: "{{ user }}"
file:
path: "{{ app_path }}/shared/config"
state: directoryindichiamo solo una directory e Ansible creerà automaticamente la directory padre, se necessario.
Ansible Vault
Ci siamo già imbattuti nel fatto che nelle variabili possono trovarsi dati riservati come la password dell'utente. Se hai creato è destinato a leggere le registrazioni dal file un file per l'applicazione, e database.yml lì dovrebbe esserci ancora più di questi dati sensibili. Sarebbe bene tenerli nascosti agli occhi indiscreti. A tal fine si utilizza .
Creiamo un file per le variabili /ansible/vars/all.yml (qui si possono creare diversi file per diversi gruppi di host, proprio come nel file inventory: production.yml, staging.yml, ecc.).
In questo file è necessario trasferire tutte le variabili che devono essere cifrate, utilizzando la sintassi standard di 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_baseDopo di che, questo file può essere cifrato con il comando:
ansible-vault encrypt ./vars/all.ymlNaturalmente, durante la cifratura sarà necessario impostare una password per la decrittazione. Puoi vedere cosa conterrà il file dopo aver eseguito questo comando.
Utilizzando ansible-vault decrypt il file può essere decriptato, modificato e poi cifrato nuovamente.
Per lavorare, non è necessario decrittare il file. Lo conservi in forma cifrata e avvii il playbook con l'argomento --ask-vault-pass. Ansible chiederà la password, recupererà le variabili ed eseguirà i compiti. Tutti i dati rimarranno cifrati.
La comando completo per diversi gruppi di host e ansible vault apparirà all'incirca così:
ansible-playbook -i inventory ./playbook.yml -l "staging" --ask-vault-passE il testo completo dei playbook e dei ruoli non te lo darò, scrivilo tu stesso. Perché ansible è così: se non capisci cosa devi fare, lui non lo farà per te.
Fonte: habr.com
