Di recente, ho avuto la necessità di scrivere alcuni playbook di Ansible per preparare il server al deploy di un'applicazione Rails. E, con mia sorpresa, non ho trovato un semplice manuale passo per passo. Non volevo copiare un playbook di qualcun altro senza comprendere ciò che stava accadendo, quindi ho finito per leggere la documentazione, raccogliendo tutto da solo. Forse, con questo articolo, posso aiutare qualcuno a velocizzare questo processo.
Innanzitutto, è importante capire che Ansible offre un'interfaccia comoda per eseguire un elenco predefinito di azioni su un server remoto (o più server) tramite SSH. Non c'è alcuna magia qui: non puoi semplicemente installare un plugin e ottenere un deploy zero 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 mi accontento dei playbook già pronti da GitHub o articoli del tipo: “Copia e incolla, funzionerà”.
Di cosa abbiamo bisogno?
Come ho già detto, per scrivere un playbook devi sapere cosa vuoi fare e come farlo. Vediamo quindi quali sono le nostre esigenze. Per un'applicazione Rails, avremo bisogno di alcuni pacchetti di sistema: nginx, postgresql (redis, ecc.). Inoltre, abbiamo bisogno di ruby di una versione specifica. È 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 ciò, dobbiamo 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:
- Accediamo come root
- installiamo i pacchetti di sistema
- creiamo un nuovo utente, impostiamo i permessi, chiave ssh
- configuriamo i pacchetti di sistema (nginx, ecc.) e li avviamo
- creiamo un utente nel database (possibilmente creando anche il database)
- accediamo come nuovo utente
- installiamo rbenv e ruby
- installiamo bundler
- carichiamo il codice dell'applicazione
- avviamo il server Puma
Tra l'altro, le fasi finali possono essere eseguite utilizzando capistrano, almeno esso è in grado di copiare il codice nelle directory di rilascio, cambiare il rilascio con un symlink dopo un deploy riuscito, copiare le configurazioni dalla directory shared, riavviare puma, ecc. Tutto questo può essere fatto anche con Ansible, ma perché farlo?
Struttura dei file
Ansible ha una rigorosa per tutti i suoi file, quindi è meglio tenere tutto in una directory separata. Non è così importante che si trovi all'interno dell'applicazione rails o separatamente. 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 tenere tutto in un unico repository.
Playbook Semplice
Il playbook è un file yml in cui viene descritto, utilizzando una sintassi speciale, cosa e come Ansible deve fare. Creiamo 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 lanciare:
ansible-playbook ./playbook.yml
PLAY [Playbook Semplice] ************************************************************************************************************************************
skipping: nessun host corrispondeAnsible dice che non conosce gli host che corrispondono alla lista all. Devono essere elencati in un apposito .
Creiamo quindi uno nella stessa directory ansible:
123.123.123.123Così semplicemente specifichiamo un host (ideale sarebbe l'host del tuo VPS per test, altrimenti puoi scrivere localhost) e lo salviamo con il nome inventory.
Puoi provare a lanciare ansible con il file di inventory:
ansible-playbook ./playbook.yml -i inventory
PLAY [Playbook Semplice] ************************************************************************************************************************************
TASK [Raccolta Dati] ************************************************************************************************************************************
PLAY RECAP ************************************************************************************************************************************Se hai accesso ssh all'host specificato, Ansible si connetterà e raccoglierà informazioni sul sistema remoto. (TASK predefinito [Raccolta Dati]) dopo di che fornirà un breve rapporto sull'esecuzione (PLAY RECAP).
Per impostazione predefinita, per la connessione viene utilizzato il nome utente con cui sei autenticato nel sistema. Probabilmente non sarà presente sull'host. Nel file playbook puoi specificare quale utente utilizzare per la connessione tramite la direttiva remote_user. Inoltre, l'informazione sul sistema remoto potrebbe spesso non essere necessaria e non vale la pena perdere tempo a raccoglierla. Questo compito può anche essere disattivato:
---
- name: Simple playbook
hosts: all
remote_user: root
become: true
gather_facts: noProva a eseguire nuovamente il playbook e assicurati che la connessione funzioni. (Se hai specificato l'utente root, devi anche indicare la direttiva become: true per ottenere privilegi elevati. Come indicato nella documentazione: become impostato a ‘true’/'yes' per attivare l'escalation dei privilegi. anche se non è del tutto chiaro il 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.
Installazione di pacchetti di sistema
Nella distribuzione standard di Ansible sono inclusi numerosi moduli per lavorare con vari pacchetti di sistema, il che ci fa risparmiare tempo nel dover scrivere script bash per ogni cosa. Ora ci occorre uno di questi moduli per aggiornare il sistema e installare i 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 bisogna sapere in anticipo cosa e come fare). Tuttavia, la sintassi sarà probabilmente simile.
Aggiungiamo al nostro playbook i primi compiti:
---
- 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: presentIl Task è proprio il compito che ansible eseguirà sui server remoti. Diamo un nome al task per tenere traccia della sua esecuzione nel log. E descriviamo, usando la sintassi del modulo specifico, cosa deve fare. In questo caso apt: update_cache=yes — indica di aggiornare i pacchetti di sistema utilizzando il modulo apt. Il secondo comando è un po' più complesso. Passiamo al modulo apt un elenco di pacchetti e diciamo che il loro state deve diventare present, cioè diciamo di installare questi pacchetti. Analogamente, possiamo dire di rimuoverli o aggiornarli, semplicemente cambiando state. Si prega di notare che per far funzionare rails con postgresql abbiamo bisogno del pacchetto postgresql-contrib, che stiamo attualmente installando. Anche questo è qualcosa di cui è necessario essere a conoscenza e fare, ansible da solo non lo farà.
Prova a eseguire nuovamente il playbook e verifica che i pacchetti vengano installati.
Creazione di nuovi utenti.
Per lavorare con gli utenti, Ansible ha anche un modulo chiamato user. Aggiungeremo un altro task (ho già nascosto le parti conosciute del playbook con dei commenti, per non doverlo copiare interamente ogni volta):
---
- name: Simple playbook
# ...
tasks:
# ...
- name: Add a new user
user:
name: my_user
shell: /bin/bash
password: "{{ 123qweasd | password_hash('sha512') }}"Stiamo creando un nuovo utente, impostandogli la shell e la password. E ci troviamo immediatamente 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. Iniziamo a spostare il nome utente e la password in variabili, e verso la fine dell'articolo mostrerò come criptare la password.
---
- name: Simple playbook
# ...
tasks:
# ...
- name: Add a new user
user:
name: "{{ user }}"
shell: /bin/bash
password: "{{ user_password | password_hash('sha512') }}"Utilizzando le doppie parentesi graffe nei playbook vengono impostate le variabili.
Indicheremo i valori delle variabili nel file di inventory:
123.123.123.123
[all:vars]
user=my_user
user_password=123qweasdNota la direttiva [all:vars] — indica che il blocco di testo successivo sono variabili (vars) e si applicano 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. Ma salva tutte le informazioni direttamente, motivo per cui anche la password deve essere preventivamente convertita in hash, come fa questo comando.
Aggiungiamo il nostro utente al gruppo sudo. Tuttavia, prima di tutto è necessario assicurarsi 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 a quella di apt. Dopodiché, è sufficiente assegnare questo gruppo all'utente (groups: "sudo").
È inoltre utile aggiungere a questo utente una chiave ssh, in modo da poter accedere senza password:
---
- name: Simple playbook
# ...
tasks:
# ...
- name: Assicurati di avere 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: Distribuisci chiave SSH
authorized_key:
user: "{{ user }}"
key: "{{ lookup('file', '~/.ssh/id_rsa.pub') }}"
state: presentIn questo caso la costruzione interessante è "{{ lookup('file', '~/.ssh/id_rsa.pub') }}" — essa copia il contenuto del file id_rsa.pub (il tuo nome potrebbe differire), cioè la parte pubblica della chiave ssh e la carica nell'elenco delle chiavi autorizzate per l'utente sul server.
Ruoli
Tutte e tre le attività per la creazione degli utenti possono facilmente essere raggruppate in un'unica categoria di attività, e sarebbe utile mantenere questo gruppo separato dal playbook principale, affinché non cresca troppo. A tale scopo, in ansible esistono .
Secondo la struttura dei file indicata all'inizio, i ruoli devono essere messi in una directory separata roles, con una directory separata per ogni ruolo 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 sarà caricato ed eseguito quando collegherai il ruolo al playbook, in esso si possono includere altri file del ruolo). 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 il ruolo user:
---
- name: Simple playbook
hosts: all
remote_user: root
gather_facts: no
tasks:
- name: Aggiorna il sistema
apt: update_cache=yes
- name: Installa le dipendenze di sistema
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 definite in pre_tasks.
Configurazione di nginx
Nginx dovrebbe essere già installato, è necessario configurarlo e avviarlo. Facciamo questo subito nel ruolo. Creiamo la struttura dei file:
- ansible
- roles
- nginx
- files
- tasks
- main.yml
- templatesOra abbiamo bisogno di file e modelli. La differenza tra di loro è che i file vengono copiati da Ansible così come sono. I modelli, invece, devono avere l'estensione j2 e possono utilizzare i valori delle variabili tramite le stesse parentesi graffe doppie.
Attiviamo nginx in main.yml file. Per farlo 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 diciamo che nginx deve essere avviato (ossia lo lanciamo), 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'Creiamo il file di configurazione principale per nginx (può essere preso direttamente dal server o scritto da noi). Inoltre, creiamo 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 modello sostituendo i valori delle variabili. Il modello deve trovarsi in /ansible/roles/nginx/templates/my_app.j2). E potrebbe assomigliare a qualcosa del genere:
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 ai segnaposto {{ app_name }}, {{ app_path }}, {{ server_name }}, {{ inventory_hostname }} queste sono tutte variabili i cui valori Ansible sostituirà nel modello prima di copiare. Questo è utile se si utilizza un playbook per diverse 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 eseguiamo il nostro playbook, eseguirà i compiti specificati per entrambi gli host. Ma per l'host staging, i variabili saranno diversi da quelli di produzione, non solo nei ruoli e nei playbook, ma anche nei file di configurazione di nginx. {{ inventory_hostname }} non è necessario indicare nel file di inventory — questa è e qui viene memorizzato l'host per cui viene eseguito il playbook in questo momento.
Se desideri avere un file di 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 di inventory separati per gruppi diversi. O si possono combinare i due approcci, se hai molti host diversi.
Torniamo alla configurazione di nginx. Dopo aver copiato i file di configurazione, dobbiamo creare un symlink in sites_enabled a my_app.conf da sites_available. E riavviare nginx.
... # codice vecchio in mail.yml
- nome: Crea symlink a sites-enabled
file:
src: /etc/nginx/sites-available/my_app.conf
dest: /etc/nginx/sites-enabled/my_app.conf
state: link
- nome: riavvia nginx
service:
name: nginx
state: restartedQui è tutto semplice: di nuovo i moduli ansible con una sintassi abbastanza standard. Ma c'è un aspetto da considerare. Non ha senso riavviare nginx ogni volta. Avete notato che non scriviamo comandi del tipo: "fai questo in questo modo", la sintassi sembra piuttosto come "questo deve avere questo stato". E di solito è così che funziona ansible. Se il gruppo esiste già, o il pacchetto di sistema è già installato, ansible lo verificherà e salterà il compito. Anche i file non verranno copiati se corrispondono completamente a ciò che è già presente sul server. Possiamo sfruttare questa caratteristica 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 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 avviare postgresql tramite systemd proprio come abbiamo fatto con nginx, e anche creare un utente che utilizzeremo per accedere al database e il 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 nell'inventory, è già stato fatto molte volte, così come la sintassi dei moduli postgresql_db e postgresql_user. Maggiori informazioni possono essere trovate nella documentazione. Qui la direttiva più interessante è become_user: postgres. Il fatto è che, per impostazione predefinita, l'accesso al database postgresql è riservato solo all'utente postgres e solo localmente. Questa direttiva ci consente di eseguire comandi a nome di questo utente (se abbiamo accesso, ovviamente).
Potrebbe anche essere necessario aggiungere una riga in pg_hba.conf per consentire l'accesso al nuovo utente al database. Questo può essere fatto proprio 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 un repository git. Quindi, questo compito diventa piuttosto atipico. Creeremo un ruolo per esso /ansible/roles/ruby_rbenv/main.yml e inizieremo 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 sotto l'utente che abbiamo creato per questo scopo. Poiché rbenv viene installato nella sua directory home e non globalmente. Utilizziamo anche il modulo git per clonare il repository, specificando repo e dest.
Successivamente dobbiamo aggiungere rbenv init nel bashrc e aggiungere rbenv nel PATH. Per questo abbiamo il modulo lineinfile:
- name: Aggiungi rbenv al PATH
become_user: "{{ user }}"
lineinfile:
path: ~\/..bashrc
state: present
line: 'export PATH="${HOME}\/..rbenv\/bin:${PATH}"'
- name: Aggiungi rbenv init al 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, installare ruby. Questo viene fatto 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\/bashIndichiamo quale comando eseguire e con quale metodo. Tuttavia, qui ci imbatteremo nel fatto che ansible non esegue il codice contenuto in bashrc prima di eseguire i comandi. Pertanto, rbenv dovrà essere definito direttamente in questo stesso script.
Il problema successivo è che il comando shell non ha uno stato dal punto di vista di ansible. Non ci sarà una verifica automatica per vedere se questa versione di ruby è stata 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 al playbook principale.
File condivisi.
In generale, a questo punto si potrebbe concludere la configurazione. Rimane da avviare capistrano e copierà automaticamente il codice, creerà le directory necessarie e avvierà l'applicazione (se tutto è configurato correttamente). Tuttavia, spesso capistrano ha bisogno di file di configurazione aggiuntivi, come database.yml o .env Possono essere copiati esattamente come file e modelli per nginx. C'è solo una piccola particolarità. Prima di copiare i file, è necessario creare una struttura di directory per essi, qualcosa di simile a questo:
# 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 le directory superiori, 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 .env un file per l'applicazione, e database.yml quindi ci devono essere ancora più dati critici. Sarebbe opportuno nasconderli agli sguardi indiscreti. A tal fine si utilizza .
Creeremo un file per le variabili /ansible/vars/all.yml (qui puoi creare diversi file per diversi gruppi di host, esattamente come nel file inventory: production.yml, staging.yml, ecc.).
In questo file è necessario trasferire tutte le variabili che devono essere criptate, 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_baseDopo di che, questo file può essere criptato con il comando:
ansible-vault encrypt ./vars/all.ymlNaturalmente, durante la crittografia sarà necessario impostare una password per la decriptazione. Puoi vedere cosa ci sarà all'interno del file dopo aver eseguito questo comando.
Utilizzando ansible-vault decrypt il file può essere decritto, modificato e poi criptato di nuovo.
Per lavorare, non è necessario decriptare il file. Lo conservi in forma criptata e avvii il playbook con l'argomento --ask-vault-pass. Ansible chiederà la password, recupererà le variabili e eseguirà i compiti. Tutti i dati rimarranno criptati.
L'intera comando per diversi gruppi di host e ansible vault apparirà approssimativamente in questo modo:
ansible-playbook -i inventory ./playbook.yml -l "staging" --ask-vault-passE non ti darò il testo completo dei playbook e dei ruoli, scrivili da solo. Perché ansible è così: se non capisci cosa devi fare, allora non lo farà nemmeno lui.
Fonte: habr.com
