Non molto tempo fa ho dovuto scrivere diversi playbook Ansible per preparare il server alla distribuzione di un'applicazione Rails. E, sorprendentemente, non ho trovato un semplice manuale passo passo. Non volevo copiare il playbook di qualcun altro senza capire cosa stava succedendo, e alla fine ho dovuto leggere la documentazione, raccogliendo tutto da solo. Forse posso aiutare qualcuno ad accelerare questo processo con l'aiuto di questo articolo.
La prima cosa da capire è che ansible fornisce una comoda interfaccia per eseguire un elenco predefinito di azioni su uno o più server remoti tramite SSH. Non c'è magia qui, non puoi installare un plugin e ottenere una distribuzione senza tempi di inattività della tua applicazione con docker, monitoraggio e altre funzionalità pronte all'uso. Per scrivere un playbook, devi sapere esattamente cosa vuoi fare e come farlo. Ecco perché non mi accontento dei playbook già pronti di GitHub o di articoli come: “Copia ed esegui, funzionerà”.
Ciò che ci serve?
Come ho già detto, per scrivere un playbook devi sapere cosa vuoi fare e come farlo. Decidiamo di cosa abbiamo bisogno. Per un'applicazione Rails avremo bisogno di diversi pacchetti di sistema: nginx, postgresql (redis, ecc.). Inoltre, abbiamo bisogno di una versione specifica di Ruby. È meglio installarlo tramite rbenv (rvm, asdf...). Eseguire tutto questo come utente root è sempre una cattiva idea, quindi è necessario creare un utente separato e configurare i suoi diritti. Successivamente, devi caricare il nostro codice sul server, copiare le configurazioni per nginx, postgres, ecc. e avviare tutti questi servizi.
Di conseguenza, la sequenza delle azioni è la seguente:
- Accedi come root
- installare i pacchetti di sistema
- crea un nuovo utente, configura i diritti, chiave ssh
- configurare i pacchetti di sistema (nginx ecc.) ed eseguirli
- Creiamo un utente nel database (puoi creare subito un database)
- Accedi come nuovo utente
- Installa rbenv e ruby
- Installazione del bundler
- Caricamento del codice dell'applicazione
- Avvio del server Puma
Inoltre, le ultime fasi possono essere eseguite utilizzando capistrano, almeno immediatamente può copiare il codice nelle directory di rilascio, cambiare la versione con un collegamento simbolico dopo la distribuzione riuscita, copiare le configurazioni da una directory condivisa, riavviare puma, ecc. Tutto questo può essere fatto utilizzando Ansible, ma perché?
Struttura dei file
Ansible è severo
Manuale semplice
Playbook è un file yml che, utilizzando una sintassi speciale, descrive cosa dovrebbe fare Ansible e come. Creiamo il primo playbook che non fa nulla:
---
- name: Simple playbook
hosts: all
Qui diciamo semplicemente che il nostro playbook si chiama Simple Playbook
e che il suo contenuto dovrebbe essere eseguito per tutti gli host. Possiamo salvarlo nella directory /ansible con il nome playbook.yml
e prova a eseguire:
ansible-playbook ./playbook.yml
PLAY [Simple Playbook] ************************************************************************************************************************************
skipping: no hosts matched
Ansible afferma di non conoscere alcun host che corrisponda all'elenco completo. Devono essere elencati in uno speciale
Creiamolo nella stessa directory ansible:
123.123.123.123
In questo modo specifichiamo semplicemente l'host (idealmente l'host del nostro VPS per i test, oppure puoi registrare localhost) e lo salviamo con il nome inventory
.
Puoi provare a eseguire ansible con un file di inventario:
ansible-playbook ./playbook.yml -i inventory
PLAY [Simple Playbook] ************************************************************************************************************************************
TASK [Gathering Facts] ************************************************************************************************************************************
PLAY RECAP ************************************************************************************************************************************
Se hai accesso ssh all'host specificato, ansible si connetterà e raccoglierà informazioni sul sistema remoto. (TASK predefinito [Gathering Facts]) dopodiché fornirà un breve rapporto sull'esecuzione (PLAY RECAP).
Per impostazione predefinita, la connessione utilizza il nome utente con cui hai effettuato l'accesso al sistema. Molto probabilmente non sarà sull'host. Nel file del playbook è possibile specificare quale utente utilizzare per connettersi utilizzando la direttiva remote_user. Inoltre, le informazioni su un sistema remoto potrebbero spesso non essere necessarie per te e non dovresti perdere tempo a raccoglierle. Questa attività può anche essere disabilitata:
---
- name: Simple playbook
hosts: all
remote_user: root
become: true
gather_facts: no
Prova a eseguire nuovamente il playbook e assicurati che la connessione funzioni. (Se hai specificato l'utente root, devi anche specificare la direttiva diventa: true per ottenere diritti elevati. Come scritto nella documentazione: become set to ‘true’/’yes’ to activate privilege escalation.
anche se non è del tutto chiaro il motivo).
Forse riceverai un errore causato dal fatto che ansible non può determinare l'interprete Python, quindi puoi specificarlo manualmente:
ansible_python_interpreter: /usr/bin/python3
Puoi scoprire dove hai Python con il comando whereis python
.
Installazione dei pacchetti di sistema
La distribuzione standard di Ansible include molti moduli per lavorare con vari pacchetti di sistema, quindi non dobbiamo scrivere script bash per nessun motivo. Ora abbiamo bisogno di uno di questi moduli per aggiornare il sistema e installare i pacchetti di sistema. Ho Ubuntu Linux sul mio VPS, quindi per installare i pacchetti utilizzo apt-get
и
Integriamo il nostro playbook con i primi compiti:
---
- 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
L'attività è esattamente l'attività che Ansible eseguirà sui server remoti. Diamo un nome all'attività in modo da poter tracciare la sua esecuzione nel registro. E descriviamo, utilizzando la sintassi di un modulo specifico, cosa deve fare. In questo caso apt: update_cache=yes
- dice di aggiornare i pacchetti di sistema usando il modulo apt. Il secondo comando è un po’ più complicato. Passiamo un elenco di pacchetti al modulo apt e diciamo che lo sono state
dovrebbe diventare present
, ovvero diciamo installare questi pacchetti. In modo simile, possiamo dire loro di cancellarli o aggiornarli semplicemente modificandoli state
. Tieni presente che affinché i rail funzionino con postgresql abbiamo bisogno del pacchetto postgresql-contrib, che stiamo installando ora. Ancora una volta, devi saperlo e farlo; ansible da solo non lo farà.
Prova a eseguire nuovamente il playbook e controlla che i pacchetti siano installati.
Creazione di nuovi utenti.
Per lavorare con gli utenti, Ansible ha anche un modulo: utente. Aggiungiamo un'altra attività (ho nascosto le parti già note del playbook dietro i commenti per non copiarlo interamente ogni volta):
---
- name: Simple playbook
# ...
tasks:
# ...
- name: Add a new user
user:
name: my_user
shell: /bin/bash
password: "{{ 123qweasd | password_hash('sha512') }}"
Creiamo un nuovo utente, impostiamo uno schema e una password per esso. E poi ci imbattiamo in diversi problemi. Cosa succede se i nomi utente devono essere diversi per host diversi? E memorizzare la password in chiaro nel playbook è una pessima idea. Per cominciare, inseriamo nome utente e password in variabili e verso la fine dell'articolo mostrerò come crittografare la password.
---
- name: Simple playbook
# ...
tasks:
# ...
- name: Add a new user
user:
name: "{{ user }}"
shell: /bin/bash
password: "{{ user_password | password_hash('sha512') }}"
Le variabili vengono impostate nei playbook utilizzando doppie parentesi graffe.
Indicheremo i valori delle variabili nel file di inventario:
123.123.123.123
[all:vars]
user=my_user
user_password=123qweasd
Si prega di notare la direttiva [all:vars]
- dice che il blocco di testo successivo è costituito da variabili (vars) e sono applicabili a tutti gli host (all).
Interessante anche il design "{{ user_password | password_hash('sha512') }}"
. Il fatto è che ansible non installa l'utente tramite user_add
come se lo faresti manualmente. E salva direttamente tutti i dati, motivo per cui dobbiamo anche convertire preventivamente la password in un hash, cosa che 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: 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"
Tutto è abbastanza semplice, abbiamo anche un modulo group per creare gruppi, con una sintassi molto simile ad apt. Quindi è sufficiente registrare questo gruppo per l'utente (groups: "sudo"
).
È anche utile aggiungere una chiave ssh a questo utente in modo da poter accedere utilizzandolo senza password:
---
- 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
In questo caso il design è interessante "{{ 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.
ruolo
Tutte e tre le attività per la creazione dell'utilizzo possono essere facilmente classificate in un unico gruppo di attività e sarebbe una buona idea archiviare questo gruppo separatamente dal playbook principale in modo che non diventi troppo grande. A questo scopo, Ansible ha
Secondo la struttura dei file indicata all'inizio, i ruoli devono essere posizionati in una directory separata dei ruoli, per ogni ruolo esiste una directory separata con lo stesso nome, all'interno della directory task, files, templates, etc
Creiamo una struttura di file: ./ansible/roles/user/tasks/main.yml
(main è il file principale che verrà caricato ed eseguito quando un ruolo è connesso al playbook; ad esso possono essere collegati altri file di ruolo). Ora puoi trasferire tutte le attività relative all'utente in questo file:
# 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
Nel playbook principale, devi specificare di utilizzare il ruolo utente:
---
- 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
Inoltre, potrebbe avere senso aggiornare il sistema prima di tutte le altre attività; per fare ciò è possibile rinominare il blocco tasks
in cui sono definiti pre_tasks
.
Configurazione di nginx
Dovremmo già avere Nginx installato; dobbiamo configurarlo ed eseguirlo. Facciamolo subito nel ruolo. Creiamo una struttura di file:
- ansible
- roles
- nginx
- files
- tasks
- main.yml
- templates
Ora abbiamo bisogno di file e modelli. La differenza tra loro è che ansible copia i file direttamente, così come sono. E i modelli devono avere l'estensione j2 e possono utilizzare valori variabili utilizzando le stesse doppie parentesi graffe.
Abilitiamo nginx in main.yml
file. Per questo abbiamo un modulo systemd:
# Copy nginx configs and start it
- name: enable service nginx and start
systemd:
name: nginx
state: started
enabled: yes
Qui non solo diciamo che nginx deve essere avviato (cioè lo lanciamo), ma diciamo subito che deve essere abilitato.
Adesso 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'
Creiamo 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 (non è necessario ma utile). Nel primo caso utilizziamo il modulo copy per copiare i file (il file deve essere in /ansible/roles/nginx/files/nginx.conf
). Nella seconda copiamo il template, sostituendo i valori delle variabili. Il modello dovrebbe essere presente /ansible/roles/nginx/templates/my_app.j2
). E potrebbe assomigliare 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 }};
....
}
Prestare attenzione agli inserti {{ app_name }}
, {{ app_path }}
, {{ server_name }}
, {{ inventory_hostname }}
— queste sono tutte le variabili i cui valori Ansible sostituirà nel modello prima di copiare. Ciò è utile se utilizzi un playbook per diversi gruppi di host. Ad esempio, possiamo aggiungere il nostro file di inventario:
[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
Se ora lanciamo il nostro playbook, eseguirà le attività specificate per entrambi gli host. Ma allo stesso tempo, per un host di staging, le variabili saranno diverse da quelle di produzione, e non solo nei ruoli e nei playbook, ma anche nelle configurazioni nginx. {{ inventory_hostname }}
non è necessario specificarlo nel file di inventario: questo
Se desideri avere un file di inventario per diversi host, ma eseguirlo solo per un gruppo, puoi farlo con il seguente comando:
ansible-playbook -i inventory ./playbook.yml -l "staging"
Un'altra opzione è avere file di inventario separati per gruppi diversi. Oppure puoi combinare i due approcci se disponi di molti host diversi.
Torniamo alla configurazione di nginx. Dopo aver copiato i file di configurazione, dobbiamo creare un collegamento simbolico in sitest_enabled a my_app.conf da sites_available. E riavvia 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
Qui tutto è semplice: ancora una volta moduli ansible con una sintassi abbastanza standard. Ma c'è un punto. Non ha senso riavviare nginx ogni volta. Hai notato che non scriviamo comandi del tipo: “fai questo in questo modo”, la sintassi assomiglia più a “questo dovrebbe avere questo stato”. E molto spesso è proprio così che funziona Ansible. Se il gruppo esiste già o il pacchetto di sistema è già installato, ansible lo controllerà e salterà l'attività. Inoltre, i file non verranno copiati se corrispondono completamente a ciò che è già presente sul server. Possiamo approfittarne e riavviare nginx solo se i file di configurazione sono stati modificati. Esiste una direttiva di registro per questo:
# 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
Se uno dei file di configurazione cambia, verrà fatta una copia e la variabile verrà registrata restart_nginx
. E solo se questa variabile è stata registrata il servizio verrà riavviato.
E, ovviamente, devi aggiungere il ruolo nginx al playbook principale.
Configurazione di postgresql
Dobbiamo abilitare postgresql usando systemd nello stesso modo in cui abbiamo fatto con nginx, e anche creare un utente che utilizzeremo per accedere al database e al database stesso.
Creiamo un 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 descriverò come aggiungere variabili all'inventario, questo è già stato fatto molte volte, così come la sintassi dei moduli postgresql_db e postgresql_user. Maggiori informazioni possono essere trovate nella documentazione. La direttiva più interessante qui è 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 per conto di questo utente (se abbiamo accesso, ovviamente).
Inoltre, potrebbe essere necessario aggiungere una riga a pg_hba.conf per consentire a un nuovo utente di accedere al database. Questo può essere fatto nello stesso modo in cui abbiamo modificato la configurazione di nginx.
E, naturalmente, è necessario aggiungere il ruolo postgresql al playbook principale.
Installazione di Ruby tramite rbenv
Ansible non dispone di moduli per lavorare con rbenv, ma viene installato clonando un repository git. Pertanto, questo problema diventa il più non standard. Creiamo un ruolo per lei /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=~/.rbenv
Utilizziamo nuovamente la direttiva diventa_utente per lavorare con l'utente che abbiamo creato per questi scopi. Poiché rbenv è installato nella sua directory home e non a livello globale. E usiamo anche il modulo git per clonare il repository, specificando repo e dest.
Successivamente, dobbiamo registrare rbenv init in bashrc e aggiungere rbenv a PATH lì. Per questo abbiamo il modulo 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 -)"'
Quindi devi installare ruby_build:
- name: Install ruby-build
become_user: "{{ user }}"
git: repo=https://github.com/rbenv/ruby-build.git dest=~/.rbenv/plugins/ruby-build
E infine installa Ruby. Questo avviene tramite rbenv, cioè semplicemente con il comando 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
Diciamo quale comando eseguire e con cosa. Qui però ci imbattiamo nel fatto che ansible non esegue il codice contenuto in bashrc prima di eseguire i comandi. Ciò significa che rbenv dovrà essere definito direttamente nello stesso script.
Il problema successivo è dovuto al fatto che il comando della shell non ha uno stato dal punto di vista ansible. Cioè, non verrà effettuato alcun controllo automatico se questa versione di Ruby è installata o meno. Possiamo farlo noi stessi:
- 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
Non resta che installare bundler:
- name: Install bundler
become_user: "{{ user }}"
shell: |
export PATH="${HOME}/.rbenv/bin:${PATH}"
eval "$(rbenv init -)"
gem install bundler
E ancora, aggiungi il nostro ruolo ruby_rbenv al playbook principale.
File condivisi.
In generale, la configurazione potrebbe essere completata qui. Successivamente non resta che eseguire capistrano che copierà il codice stesso, creerà le directory necessarie e avvierà l'applicazione (se tutto è configurato correttamente). Tuttavia, capistrano spesso richiede file di configurazione aggiuntivi, come database.yml
o .env
Possono essere copiati proprio come file e modelli per nginx. C'è solo una sottigliezza. Prima di copiare i file, devi creare una struttura di directory per loro, qualcosa del genere:
# Copy shared files for deploy
- name: Ensure shared dir
become_user: "{{ user }}"
file:
path: "{{ app_path }}/shared/config"
state: directory
specifichiamo solo una directory e ansible creerà automaticamente quelle principali, se necessario.
Volta Ansible
Abbiamo già riscontrato che le variabili possono contenere dati segreti come la password dell’utente. Se hai creato .env
file per la domanda e database.yml
allora devono esserci ancora più dati critici di questo tipo. Sarebbe bene nasconderli da occhi indiscreti. A questo scopo viene utilizzato
Creiamo un file per le variabili /ansible/vars/all.yml
(qui puoi creare file diversi per diversi gruppi di host, proprio come nel file di inventario: production.yml, staging.yml, ecc.).
Tutte le variabili che devono essere crittografate devono essere trasferite in questo file utilizzando la sintassi yml standard:
# 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
Dopodiché questo file può essere crittografato con il comando:
ansible-vault encrypt ./vars/all.yml
Naturalmente, durante la crittografia dovrai impostare una password per la decrittografia. Puoi vedere cosa ci sarà all'interno del file dopo aver chiamato questo comando.
Per mezzo di ansible-vault decrypt
il file può essere decrittografato, modificato e quindi nuovamente crittografato.
Non è necessario decrittografare il file per funzionare. Lo memorizzi crittografato ed esegui il playbook con l'argomento --ask-vault-pass
. Ansible chiederà la password, recupererà le variabili ed eseguirà le attività. Tutti i dati rimarranno crittografati.
Il comando completo per diversi gruppi di host e ansible vault sarà simile al seguente:
ansible-playbook -i inventory ./playbook.yml -l "staging" --ask-vault-pass
Ma non ti darò il testo completo di playbook e ruoli, scrivilo tu stesso. Perché ansible è così: se non capisci cosa deve essere fatto, non lo farà per te.
Fonte: habr.com