For ikke længe siden havde jeg brug for at skrive flere Ansible-spilbøger for at forberede serveren til at implementere en Rails-applikation. Og overraskende nok fandt jeg ikke en simpel trin-for-trin manual. Jeg ville ikke kopiere en andens spillebog uden at forstå, hvad der skete, og til sidst måtte jeg læse dokumentationen og samle alting selv. Måske kan jeg hjælpe nogen med at fremskynde denne proces ved hjælp af denne artikel.
Den første ting at forstå er, at ansible giver dig en praktisk grænseflade til at udføre en foruddefineret liste over handlinger på en eller flere fjernservere via SSH. Der er ingen magi her, du kan ikke installere et plugin og få en nul nedetidsimplementering af din applikation med docker, overvågning og andre godbidder ud af boksen. For at kunne skrive en legebog skal du vide, hvad du præcist vil gøre, og hvordan du gør det. Derfor er jeg ikke tilfreds med færdige spillebøger fra GitHub eller artikler som: "Kopier og kør, det vil fungere."
Hvad har vi brug for?
Som jeg allerede har sagt, for at skrive en playbook skal du vide, hvad du vil gøre, og hvordan du gør det. Lad os beslutte, hvad vi har brug for. Til en Rails-applikation skal vi bruge flere systempakker: nginx, postgresql (redis osv.). Derudover har vi brug for en specifik version af rubin. Det er bedst at installere det via rbenv (rvm, asdf...). At køre alt dette som root-bruger er altid en dårlig idé, så du skal oprette en separat bruger og konfigurere hans rettigheder. Herefter skal du uploade vores kode til serveren, kopiere konfigurationerne for nginx, postgres osv. og starte alle disse tjenester.
Som et resultat er rækkefølgen af handlinger som følger:
- Log ind som root
- installere systempakker
- oprette en ny bruger, konfigurere rettigheder, ssh nøgle
- konfigurer systempakker (nginx osv.) og kør dem
- Vi opretter en bruger i databasen (du kan straks oprette en database)
- Log ind som ny bruger
- Installer rbenv og ruby
- Installation af bundler
- Uploader applikationskoden
- Starter Puma-serveren
Desuden kan de sidste trin udføres ved hjælp af capistrano, i det mindste ud af boksen kan den kopiere kode til udgivelsesmapper, skifte udgivelsen med et symlink efter vellykket implementering, kopiere konfigurationer fra en delt mappe, genstarte puma osv. Alt dette kan gøres ved hjælp af Ansible, men hvorfor?
Filstruktur
Ansible har strenge
Simpel Playbook
Playbook er en yml-fil, der ved hjælp af speciel syntaks beskriver, hvad Ansible skal gøre og hvordan. Lad os skabe den første spillebog, der ikke gør noget:
---
- name: Simple playbook
hosts: all
Her siger vi blot, at vores spillebog hedder Simple Playbook
og at dens indhold skal udføres for alle værter. Vi kan gemme det i /ansible mappe med navnet playbook.yml
og prøv at køre:
ansible-playbook ./playbook.yml
PLAY [Simple Playbook] ************************************************************************************************************************************
skipping: no hosts matched
Ansible siger, at den ikke kender nogen værter, der matcher alle listen. De skal være opført i en særlig
Lad os oprette det i den samme mappe:
123.123.123.123
Sådan angiver vi simpelthen værten (ideelt set værten for vores VPS til test, eller du kan registrere localhost) og gemmer den under navnet inventory
.
Du kan prøve at køre ansible med en opgørelsesfil:
ansible-playbook ./playbook.yml -i inventory
PLAY [Simple Playbook] ************************************************************************************************************************************
TASK [Gathering Facts] ************************************************************************************************************************************
PLAY RECAP ************************************************************************************************************************************
Hvis du har ssh-adgang til den angivne vært, vil ansible oprette forbindelse og indsamle oplysninger om fjernsystemet. (standard OPGAVE [Gathing Facts]), hvorefter den vil give en kort rapport om udførelsen (PLAY RECAP).
Som standard bruger forbindelsen det brugernavn, som du er logget ind på systemet under. Det vil højst sandsynligt ikke være på værten. I playbook-filen kan du angive, hvilken bruger der skal bruges til at oprette forbindelse ved hjælp af remote_user-direktivet. Desuden kan oplysninger om et fjernsystem ofte være unødvendige for dig, og du bør ikke spilde tid på at indsamle dem. Denne opgave kan også deaktiveres:
---
- name: Simple playbook
hosts: all
remote_user: root
become: true
gather_facts: no
Prøv at køre afspilningsbogen igen, og sørg for, at forbindelsen fungerer. (Hvis du har angivet root-brugeren, skal du også angive direktivet bliver: true for at opnå forhøjede rettigheder. Som skrevet i dokumentationen: become set to ‘true’/’yes’ to activate privilege escalation.
selvom det ikke er helt klart hvorfor).
Måske vil du modtage en fejl forårsaget af det faktum, at ansible ikke kan bestemme Python-fortolkeren, så kan du angive det manuelt:
ansible_python_interpreter: /usr/bin/python3
Du kan finde ud af, hvor du har python med kommandoen whereis python
.
Installation af systempakker
Ansibles standarddistribution indeholder mange moduler til at arbejde med forskellige systempakker, så vi behøver ikke at skrive bash-scripts af nogen grund. Nu har vi brug for et af disse moduler til at opdatere systemet og installere systempakker. Jeg har Ubuntu Linux på min VPS, så for at installere pakker bruger jeg apt-get
и
Lad os supplere vores spillebog med de første opgaver:
---
- 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
Opgave er præcis den opgave, som Ansible vil udføre på fjernservere. Vi giver opgaven et navn, så vi kan spore dens udførelse i loggen. Og vi beskriver, ved hjælp af syntaksen for et specifikt modul, hvad det skal gøre. I dette tilfælde apt: update_cache=yes
- siger at opdatere systempakker ved hjælp af apt-modulet. Den anden kommando er lidt mere kompliceret. Vi sender en liste over pakker til apt-modulet og siger, at de er state
burde blive present
, det vil sige, vi siger installere disse pakker. På lignende måde kan vi bede dem om at slette dem, eller opdatere dem ved blot at ændre state
. Bemærk venligst, at for at skinner skal fungere med postgresql, har vi brug for postgresql-contrib-pakken, som vi installerer nu. Igen, du har brug for at vide og gøre dette; ansible på egen hånd vil ikke gøre dette.
Prøv at køre afspilningsbogen igen og kontroller, at pakkerne er installeret.
Oprettelse af nye brugere.
For at arbejde med brugere har Ansible også et modul - bruger. Lad os tilføje en opgave mere (jeg gemte de allerede kendte dele af spillebogen bag kommentarerne for ikke at kopiere den helt hver gang):
---
- name: Simple playbook
# ...
tasks:
# ...
- name: Add a new user
user:
name: my_user
shell: /bin/bash
password: "{{ 123qweasd | password_hash('sha512') }}"
Vi opretter en ny bruger, sætter en schell og adgangskode til den. Og så støder vi ind i flere problemer. Hvad hvis brugernavne skal være forskellige for forskellige værter? Og at gemme adgangskoden i klartekst i spillebogen er en meget dårlig idé. Lad os til at begynde med lægge brugernavnet og adgangskoden ind i variabler, og mod slutningen af artiklen vil jeg vise, hvordan man krypterer adgangskoden.
---
- name: Simple playbook
# ...
tasks:
# ...
- name: Add a new user
user:
name: "{{ user }}"
shell: /bin/bash
password: "{{ user_password | password_hash('sha512') }}"
Variabler er sat i playbooks ved hjælp af dobbelte krøllede seler.
Vi vil angive værdierne af variablerne i inventarfilen:
123.123.123.123
[all:vars]
user=my_user
user_password=123qweasd
Bemærk venligst direktivet [all:vars]
- der står, at den næste tekstblok er variabler (vars), og de gælder for alle værter (alle).
Designet er også interessant "{{ user_password | password_hash('sha512') }}"
. Sagen er, at ansible ikke installerer brugeren via user_add
som du ville gøre det manuelt. Og det gemmer alle data direkte, hvorfor vi også skal konvertere adgangskoden til en hash på forhånd, hvilket er hvad denne kommando gør.
Lad os tilføje vores bruger til sudo-gruppen. Men før dette er vi nødt til at sikre os, at en sådan gruppe eksisterer, fordi ingen vil gøre dette for os:
---
- 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"
Alt er ret simpelt, vi har også et gruppemodul til at oprette grupper, med en syntaks meget lig apt. Så er det nok at registrere denne gruppe til brugeren (groups: "sudo"
).
Det er også nyttigt at tilføje en ssh-nøgle til denne bruger, så vi kan logge ind med den uden adgangskode:
---
- 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
I dette tilfælde er designet interessant "{{ lookup('file', '~/.ssh/id_rsa.pub') }}"
— den kopierer indholdet af filen id_rsa.pub (dit navn kan være anderledes), det vil sige den offentlige del af ssh-nøglen og uploader den til listen over autoriserede nøgler for brugeren på serveren.
rolle
Alle tre opgaver til at skabe brug kan nemt klassificeres i én gruppe af opgaver, og det vil være en god idé at gemme denne gruppe adskilt fra hovedspillebogen, så den ikke vokser sig for stor. Til dette formål har Ansible
I henhold til filstrukturen angivet helt i begyndelsen, skal roller placeres i en separat rollemappe, for hver rolle er der en separat mappe med samme navn, inde i mappen opgaver, filer, skabeloner osv.
Lad os oprette en filstruktur: ./ansible/roles/user/tasks/main.yml
(main er hovedfilen, der vil blive indlæst og udført, når en rolle er forbundet til spillebogen; andre rollefiler kan forbindes til den). Nu kan du overføre alle opgaver relateret til brugeren til denne fil:
# 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
I hovedafspilningsbogen skal du angive for at bruge brugerrollen:
---
- 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
Det kan også give mening at opdatere systemet før alle andre opgaver; for at gøre dette kan du omdøbe blokken tasks
hvor de er defineret i pre_tasks
.
Opsætning af nginx
Vi burde allerede have Nginx installeret; vi skal konfigurere det og køre det. Lad os gøre det med det samme i rollen. Lad os oprette en filstruktur:
- ansible
- roles
- nginx
- files
- tasks
- main.yml
- templates
Nu har vi brug for filer og skabeloner. Forskellen mellem dem er, at ansible kopierer filerne direkte, som de er. Og skabeloner skal have j2-udvidelsen, og de kan bruge variable værdier ved at bruge de samme dobbelte krøllede klammeparenteser.
Lad os aktivere nginx main.yml
fil. Til dette har vi et systemmodul:
# Copy nginx configs and start it
- name: enable service nginx and start
systemd:
name: nginx
state: started
enabled: yes
Her siger vi ikke kun, at nginx skal startes (det vil sige, vi starter det), men vi siger straks, at det skal aktiveres.
Lad os nu kopiere konfigurationsfilerne:
# 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'
Vi opretter den primære nginx-konfigurationsfil (du kan tage den direkte fra serveren eller skrive den selv). Og også konfigurationsfilen til vores applikation i mappen sites_available (dette er ikke nødvendigt, men nyttigt). I det første tilfælde bruger vi kopimodulet til at kopiere filer (filen skal være i /ansible/roles/nginx/files/nginx.conf
). I den anden kopierer vi skabelonen og erstatter variablernes værdier. Skabelonen skal være i /ansible/roles/nginx/templates/my_app.j2
). Og det kan se sådan ud:
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 }};
....
}
Vær opmærksom på indsatserne {{ app_name }}
, {{ app_path }}
, {{ server_name }}
, {{ inventory_hostname }}
- disse er alle de variabler, hvis værdier Ansible vil erstatte i skabelonen før kopiering. Dette er nyttigt, hvis du bruger en spillebog til forskellige grupper af værter. For eksempel kan vi tilføje vores lagerfil:
[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
Hvis vi nu starter vores playbook, vil den udføre de angivne opgaver for begge værter. Men på samme tid, for en iscenesættelsesvært, vil variablerne være forskellige fra produktionsvariablerne, og ikke kun i roller og spillebøger, men også i nginx-konfigurationer. {{ inventory_hostname }}
behøver ikke angives i inventarfilen - dette
Hvis du vil have en inventarfil til flere værter, men kun køre for én gruppe, kan dette gøres med følgende kommando:
ansible-playbook -i inventory ./playbook.yml -l "staging"
En anden mulighed er at have separate inventarfiler for forskellige grupper. Eller du kan kombinere de to tilgange, hvis du har mange forskellige værter.
Lad os gå tilbage til opsætning af nginx. Efter at have kopieret konfigurationsfilerne, skal vi oprette et symbollink i sitest_enabled til my_app.conf fra sites_available. Og genstart 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
Alt er enkelt her - igen mulige moduler med en nogenlunde standard syntaks. Men der er én pointe. Det nytter ikke at genstarte nginx hver gang. Har du bemærket, at vi ikke skriver kommandoer som: "gør det sådan her", syntaksen ligner mere "dette burde have denne tilstand". Og oftest er det præcis sådan ansible fungerer. Hvis gruppen allerede eksisterer, eller systempakken allerede er installeret, vil ansible tjekke for dette og springe opgaven over. Filer vil heller ikke blive kopieret, hvis de helt matcher det, der allerede er på serveren. Vi kan kun drage fordel af dette og genstarte nginx, hvis konfigurationsfilerne er blevet ændret. Der er et registerdirektiv for dette:
# 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
Hvis en af konfigurationsfilerne ændres, vil der blive lavet en kopi, og variablen vil blive registreret restart_nginx
. Og kun hvis denne variabel er blevet registreret, genstartes tjenesten.
Og selvfølgelig skal du tilføje nginx-rollen til hovedspillebogen.
Opsætning af postgresql
Vi skal aktivere postgresql ved hjælp af systemd på samme måde, som vi gjorde med nginx, og også oprette en bruger, som vi vil bruge til at få adgang til databasen og selve databasen.
Lad os skabe en rolle /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 }}"
Jeg vil ikke beskrive, hvordan man tilføjer variabler til inventar, dette er allerede blevet gjort mange gange, samt syntaksen for postgresql_db og postgresql_user modulerne. Yderligere information kan findes i dokumentationen. Det mest interessante direktiv her er become_user: postgres
. Faktum er, at som standard er det kun postgres-brugeren, der har adgang til postgresql-databasen og kun lokalt. Dette direktiv giver os mulighed for at udføre kommandoer på vegne af denne bruger (hvis vi selvfølgelig har adgang).
Du skal muligvis også tilføje en linje til pg_hba.conf for at give en ny bruger adgang til databasen. Dette kan gøres på samme måde som vi ændrede nginx-konfigurationen.
Og selvfølgelig skal du tilføje postgresql-rollen til hovedafspilningsbogen.
Installation af ruby via rbenv
Ansible har ikke moduler til at arbejde med rbenv, men det installeres ved at klone et git-lager. Derfor bliver dette problem det mest ikke-standardiserede. Lad os skabe en rolle for hende /ansible/roles/ruby_rbenv/main.yml
og lad os begynde at udfylde det:
# Install rbenv and ruby
- name: Install rbenv
become_user: "{{ user }}"
git: repo=https://github.com/rbenv/rbenv.git dest=~/.rbenv
Vi bruger igen bliver_bruger-direktivet til at arbejde under den bruger, vi oprettede til disse formål. Da rbenv er installeret i sin hjemmemappe, og ikke globalt. Og vi bruger også git-modulet til at klone repository, specificere repo og dest.
Dernæst skal vi registrere rbenv init i bashrc og tilføje rbenv til PATH der. Til dette har vi lineinfile-modulet:
- 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 -)"'
Så skal du installere ruby_build:
- name: Install ruby-build
become_user: "{{ user }}"
git: repo=https://github.com/rbenv/ruby-build.git dest=~/.rbenv/plugins/ruby-build
Og til sidst installere rubin. Dette gøres gennem rbenv, det vil sige blot med bash-kommandoen:
- name: Install ruby
become_user: "{{ user }}"
shell: |
export PATH="${HOME}/.rbenv/bin:${PATH}"
eval "$(rbenv init -)"
rbenv install {{ ruby_version }}
args:
executable: /bin/bash
Vi siger, hvilken kommando vi skal udføre og med hvad. Men her støder vi på det faktum, at ansible ikke kører koden indeholdt i bashrc, før de kører kommandoerne. Det betyder, at rbenv skal defineres direkte i det samme script.
Det næste problem skyldes det faktum, at shell-kommandoen ikke har nogen tilstand fra et ansible synspunkt. Det vil sige, der vil ikke være nogen automatisk kontrol, om denne version af ruby er installeret eller ej. Vi kan selv gøre dette:
- 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
Tilbage er blot at installere bundler:
- name: Install bundler
become_user: "{{ user }}"
shell: |
export PATH="${HOME}/.rbenv/bin:${PATH}"
eval "$(rbenv init -)"
gem install bundler
Og igen, føj vores rolle ruby_rbenv til hovedspillebogen.
Delte filer.
Generelt kunne opsætningen gennemføres her. Dernæst er der kun tilbage at køre capistrano, og det vil kopiere selve koden, oprette de nødvendige mapper og starte applikationen (hvis alt er konfigureret korrekt). Capistrano kræver dog ofte yderligere konfigurationsfiler, som f.eks database.yml
eller .env
De kan kopieres ligesom filer og skabeloner til nginx. Der er kun én subtilitet. Før du kopierer filer, skal du oprette en mappestruktur for dem, sådan noget som dette:
# Copy shared files for deploy
- name: Ensure shared dir
become_user: "{{ user }}"
file:
path: "{{ app_path }}/shared/config"
state: directory
vi angiver kun én mappe, og ansible vil automatisk oprette overordnede mapper, hvis det er nødvendigt.
Ansible Vault
Vi er allerede stødt på, at variabler kan indeholde hemmelige data såsom brugerens adgangskode. Hvis du har oprettet .env
fil til ansøgningen, og database.yml
så skal der være endnu flere sådanne kritiske data. Det ville være godt at skjule dem for nysgerrige øjne. Til dette formål bruges det
Lad os oprette en fil til variabler /ansible/vars/all.yml
(her kan du oprette forskellige filer til forskellige grupper af værter, ligesom i inventarfilen: production.yml, staging.yml osv.).
Alle variabler, der skal krypteres, skal overføres til denne fil ved hjælp af standard yml-syntaks:
# 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
Hvorefter denne fil kan krypteres med kommandoen:
ansible-vault encrypt ./vars/all.yml
Når du krypterer, skal du naturligvis angive en adgangskode til dekryptering. Du kan se, hvad der vil være inde i filen efter at have kaldt denne kommando.
Ved hjælp af ansible-vault decrypt
filen kan dekrypteres, ændres og derefter krypteres igen.
Du behøver ikke at dekryptere filen for at virke. Du gemmer den krypteret og kører spillebogen med argumentet --ask-vault-pass
. Ansible vil bede om adgangskoden, hente variablerne og udføre opgaverne. Alle data forbliver krypteret.
Den komplette kommando for flere grupper af værter og mulige hvælvinger vil se nogenlunde sådan ud:
ansible-playbook -i inventory ./playbook.yml -l "staging" --ask-vault-pass
Men jeg vil ikke give dig den fulde tekst af spillebøger og roller, skriv det selv. Fordi ansible er sådan - hvis du ikke forstår, hvad der skal gøres, så vil det ikke gøre det for dig.
Kilde: www.habr.com