For ikke så længe siden havde jeg brug for at skrive et par ansible playbooks til at forberede serveren til deployment af et Rails-applikation. Og, til min overraskelse, fandt jeg ikke nogen simpel trin-for-trin manual. Jeg ønskede ikke at kopiere andres playbooks uden at forstå, hvad der skete, så jeg måtte endevende dokumentationen og samle det hele selv. Måske kan jeg hjælpe nogen med at fremskynde denne proces gennem denne artikel.
Først og fremmest er det vigtigt at forstå, at Ansible giver dig et brugervenligt interface til at udføre en foruddefineret liste af handlinger på en fjernserver (eller flere) via SSH. Der er ingen magi her; du kan ikke installere et plugin og få zero downtime deployment af din applikation med Docker, overvågning og andre fordele lige ud af boksen. For at skrive et playbook skal du vide, hvad du vil gøre, og hvordan du vil gøre det. Derfor er jeg ikke tilfreds med færdige playbooks fra GitHub eller artikler som: 'Kopier og kør, så virker det'.
Hvad har vi brug for?
Som jeg allerede har nævnt, skal du vide, hvad du vil gøre, og hvordan du vil gøre det for at skrive et playbook. Lad os finde ud af, hvad vi har brug for. Til et Rails-applikation får vi brug for nogle systempakker: nginx, postgresql (redis osv.). Desuden har vi brug for en bestemt version af Ruby. Det er bedst at installere det via rbenv (rvm, asdf…). At køre det hele som root-bruger er altid en dårlig idé, derfor skal der oprettes en separat bruger med de rigtige rettigheder. Derefter skal vi uploade vores kode til serveren, kopiere konfigurationerne for nginx, postgres osv. og starte alle disse tjenester.
I sidste ende er rækkefølgen af handlinger som følger:
- Logger ind som root
- installere systempakker
- oprette en ny bruger, indstille rettigheder, SSH-nøgle
- konfigurere systempakker (nginx osv.) og starte dem
- Oprette en bruger i databasen (kan også oprettes en database med det samme)
- Logger ind som ny bruger
- Installerer rbenv og ruby
- Installerer bundler
- Uploader applikationskoden
- Starter Puma-serveren
Og de sidste trin kan gøres ved hjælp af Capistrano, i det mindste kan det som standard kopiere koden ind i release-mapperne, skifte release via et symbolsk link ved succesfuld deployment, kopiere konfigurationer fra en delt mappe, genstart puma osv. Alt dette kan også gøres med Ansible, men hvorfor?
Filstruktur
Ansible har en streng for alle sine filer, så det er bedst at holde alt dette i en separat mappe. Det er ikke så vigtigt, om den er inde i rails-appen eller separat. Man kan gemme filer i et separat git-repository. Personligt fandt jeg det lettest at oprette en ansible-mappe i /config-mappen i rails-appen og gemme alt i ét repository.
Simple Playbook
En playbook er en yml-fil, hvori der med en særlig syntaks beskrives, hvad og hvordan ansible skal gøre. Lad os oprette den første playbook, der ikke gør noget:
---
- name: Simple playbook
hosts: allHer siger vi blot, at vores playbook hedder Simple Playbook og at dens indhold skal udføres for alle værter. Vi kan gemme den i /ansible-mappen med navnet playbook.yml og prøve at køre:
ansible-playbook ./playbook.yml
PLAY [Simple Playbook] ************************************************************************************************************************************
skipping: no hosts matchedAnsible siger, at den ikke kender værter, der svarer til listen all. De skal specificeres i en særlig .
Lad os oprette den i den samme ansible-mappe:
123.123.123.123Sådan angiver vi blot en vært (ideelt set din VPS-vært til tests, eller man kan skrive localhost) og gemme den under navnet inventory.
Man kan prøve at køre ansible med inventory-filen:
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 information om det fjerne system. (standard TASK [Gathering Facts]) hvorefter det vil give en kort rapport om udførelsen (PLAY RECAP).
Som standard bruges navnet på den bruger, du er logget på systemet med, til at oprette forbindelse. På værten vil det sandsynligvis ikke være tilfældet. I playbook-filen kan man angive, hvilken bruger der skal bruges til at oprette forbindelse ved hjælp af direktivet remote_user. Desuden kan informationen om det fjerne system ofte være unødvendig, og man bør ikke spilde tid på at indsamle den. Denne opgave kan også slukkes:
---
- name: Simple playbook
hosts: all
remote_user: root
become: true
gather_facts: noPrøv at køre playbook'en igen og sikre dig, at forbindelsen fungerer. (Hvis du angav root-brugeren, skal du også angive direktivet become: true for at få forhøjede rettigheder. Som nævnt i dokumentationen: become sat til ‘true’/’yes’ for at aktivere privilegieforhøjelse. selvom det ikke helt er klart, hvorfor).
Måske får du en fejl forårsaget af, at Ansible ikke kan bestemme Python-fortolkeren, så denne kan angives manuelt:
ansible_python_interpreter: /usr/bin/python3 hvor du kan finde Python, kan du finde ud af med kommandoen whereis python.
Installation af systempakker
Den standardinstallation af Ansible indeholder mange moduler til at arbejde med forskellige systempakker, hvilket betyder, at vi ikke behøver at skrive bash-scripts for enhver anledning. Nu har vi brug for et af disse moduler til at opdatere systemet og installere systempakker. På min VPS kører Ubuntu Linux, så jeg bruger apt-get og . Hvis du bruger et andet operativsystem, kan det være nødvendigt med et andet modul (huske, jeg sagde i starten, at vi skal vide, hvad og hvordan vi skal gøre). Dog vil syntaksen sandsynligvis være lignende.
Lad os tilføje de første opgaver til vores playbook:
---
- name: Enkel playbook
hosts: all
remote_user: root
become: true
gather_facts: no
tasks:
- name: Opdater system
apt: update_cache=yes
- name: Installer systemafhængigheder
apt:
name: git,nginx,redis,postgresql,postgresql-contrib
state: presentTask — er netop den opgave, som Ansible vil udføre på de fjerntliggende servere. Vi giver opgaven et navn for at spore dens udførelse i loggen. Og beskriver, ved hjælp af modulens syntaks, hvad den skal gøre. I dette tilfælde apt: update_cache=yes — instruerer om at opdatere systempakkerne ved hjælp af apt-modulet. Den anden kommando er lidt mere kompleks. Vi sender en liste over pakker til apt-modulet og fortæller, at deres state tilstand skal være present, det vil sige, at vi beder om at installere disse pakker. På lignende måde kan vi angive, at de skal fjernes eller opdateres, blot ved at ændre state. Vær opmærksom på, at for at Rails skal fungere med PostgreSQL, har vi brug for pakken postgresql-contrib, som vi nu installerer. Dette skal man igen vide og gøre; Ansible vil ikke selv gøre det.
Prøv at køre playbook'en igen og kontroller, at pakkerne bliver installeret.
Oprettelse af nye brugere.
For working with users, Ansible also has a module called user. Let's add another task (I've hidden the known parts of the playbook in comments to avoid copying it entirely each time):
---
- name: Simple playbook
# ...
tasks:
# ...
- name: Add a new user
user:
name: my_user
shell: /bin/bash
password: "{{ 123qweasd | password_hash('sha512') }}"We create a new user, set their shell and password. But we immediately encounter several problems. What if the usernames need to be different for different hosts? Plus, storing passwords in plain text within the playbook is a very bad idea. For starters, let's move the username and password to variables, and later in the article, I'll show you how to encrypt the password.
---
- name: Simple playbook
# ...
tasks:
# ...
- name: Add a new user
user:
name: "{{ user }}"
shell: /bin/bash
password: "{{ user_password | password_hash('sha512') }}"With double curly braces, variables are set in playbooks.
We will specify the variable values in the inventory file:
123.123.123.123
[all:vars]
user=my_user
user_password=123qweasdNote the directive [all:vars] — it indicates that the following block of text consists of variables (vars) applicable to all hosts (all).
The construction is also interesting "{{ user_password | password_hash('sha512') }}". The thing is that ansible does not create users using user_add as you would do it manually. It saves all data directly, which is why we also need to pre-process the password into a hash, which this command does.
Let's add our user to the sudo group. However, before that, we need to ensure that such a group exists because no one will do this for us:
---
- 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"It's pretty straightforward; we also have a group module for creating groups, with syntax very similar to apt. After that, we just need to assign this group to the user (groups: "sudo").
It's also useful to add an SSH key for this user so that we can log in as them without a password:
---
- name: Simple playbook
# ...
tasks:
# ...
- name: Sørg for en 'sudo' gruppe
group:
name: sudo
state: present
- name: Tilføj en ny bruger
user:
name: "{{ user }}"
shell: /bin/bash
password: "{{ user_password | password_hash('sha512') }}"
groups: "sudo"
- name: Udrul SSH-nøgle
authorized_key:
user: "{{ user }}"
key: "{{ lookup('file', '~/.ssh/id_rsa.pub') }}"
state: presentI denne sammenhæng er strukturen interessant "{{ lookup('file', '~/.ssh/id_rsa.pub') }}" — den kopierer indholdet af filen id_rsa.pub (ditt navn kan variere), det vil sige den offentlige del af SSH-nøglen og uploader den til listen over autoriserede nøgler for brugeren på serveren.
Roller
Alle tre opgaver til oprettelse af brugeren kan let henføres til en gruppe opgaver, og det ville være godt at gemme denne gruppe separeret fra hovedplaybooken, så den ikke bliver for stor. Der findes .
Ifølge den angivne filstruktur i starten, skal roller placeres i en separat mappe roles, for hver rolle — en separat mappe med et tilsvarende navn, inde i mapperne tasks, files, templates osv.
Lad os oprette filstrukturen: ./ansible/roles/user/tasks/main.yml (main — dette er hovedfilen, der vil blive indlæst og udført, når rollen tilsluttes playbooken, her kan du importere andre filer fra rollen). Nu kan vi overføre alle brugerrelaterede opgaver 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: presentI hovedplaybooken skal du angive at bruge rollen user:
---
- name: Simple playbook
hosts: all
remote_user: root
gather_facts: no
tasks:
- name: Opdater systemet
apt: update_cache=yes
- name: Installer systemafhængigheder
apt:
name: git,nginx,redis,postgresql,postgresql-contrib
state: present
roles:
- userDet kan også give mening at udføre systemopdateringen før nogen af de andre opgaver, for det kan du omdøbe blokken tasks hvor de er defineret til pre_tasks.
Konfiguration af nginx
Nginx skal allerede være installeret, det er nødvendigt at konfigurere og starte det. Lad os gøre dette med det samme i rollen. Vi opretter filstrukturen:
- ansible
- roles
- nginx
- files
- tasks
- main.yml
- templatesNu har vi brug for filer og skabeloner. Forskellen mellem dem er, at ansible kopierer filer direkte, som de er. Skabeloner skal have .j2-udvidelsen, og i dem kan du bruge variabelværdier ved hjælp af de samme dobbelte krøllede parenteser.
Lad os inkludere nginx i main.yml filen. Til dette har vi modulerne systemd:
# Copy nginx configs and start it
- name: enable service nginx and start
systemd:
name: nginx
state: started
enabled: yesHer siger vi ikke kun, at nginx skal være started (det vil sige, vi starter det), men vi siger med det samme, at det skal være enabled.
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 hovedkonfigurationsfilen for nginx (du kan hente den direkte fra serveren eller skrive den selv). Og vi opretter også konfigurationsfilen for vores applikation i sites_available biblioteket (det er ikke nødvendigt, men nyttigt). I det første tilfælde bruger vi copy modulet til at kopiere filer (filen skal ligge i /ansible/roles/nginx/files/nginx.conf). I det andet tilfælde kopierer vi skabelonen med de tilsvarende variabler. Skabelonen skal ligge i /ansible/roles/nginx/templates/my_app.j2). Den kan se nogenlunde 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 }};
....
}Bemærk indsatserne {{ app_name }}, {{ app_path }}, {{ server_name }}, {{ inventory_hostname }} — dette er alle variabler, hvis værdier ansible vil indsætte i skabelonen før kopiering. Dette er nyttigt, hvis du bruger en playbook til forskellige grupper af værter. For eksempel kan vi tilføje vores inventory fil:
[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_appHvis vi nu kører vores playbook, vil den udføre de angivne opgaver for begge værter. Men for staging værten vil variablerne være forskellige fra produktion, ikke kun i roller og playbooks, men også i nginx konfigurationer. {{ inventory_hostname }} Du behøver ikke at angive i inventory filen — dette er hvor værten, som playbooken udføres på, opbevares i øjeblikket.
Hvis du ønsker at have en inventory fil for flere værter, men kun køre for en gruppe, kan du gøre det med følgende kommando:
ansible-playbook -i inventory .\/playbook.yml -l "staging"et andet alternativ — at have separate inventory filer for forskellige grupper. Eller du kan kombinere de to tilgange, hvis du har mange forskellige værter.
Lad os vende tilbage til nginx opsætningen. Efter at have kopieret konfigurationsfilerne er vi nødt til at oprette et symlink i sites_enabled til my_app.conf fra sites_available. Og genstarte nginx.
... # gammel kode i mail.yml
- name: Opret symlink til sites-enabled
file:
src: \/etc\/nginx\/sites-available\/my_app.conf
dest: \/etc\/nginx\/sites-enabled\/my_app.conf
state: link
- name: genstart nginx
service:
name: nginx
state: restartedDet er enkelt her – endnu en ansible-modul med en temmelig standard syntaks. Men der er én ting. Det giver ikke mening at genstarte nginx hver gang. Du har måske bemærket, at vi ikke skriver kommandoer som: «gør det på denne måde», syntaksen ser snarere ud som «det her skal have denne tilstand». Og oftest arbejder ansible netop på denne måde. Hvis gruppen allerede eksisterer, eller systempakken allerede er installeret, så vil ansible tjekke dette og springe opgaven over. Filer vil heller ikke blive kopieret, hvis de helt fremgår som dem, der allerede er på serveren. Vi kan udnytte dette og genstarte nginx kun, hvis konfigurationsfilerne er blevet ændret. Til dette formål er der en register-direktiv:
# 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.changedHvis en af konfigurationsfilerne ændres, så vil kopiering blive udført, og en variabel vil blive registreret restart_nginx. Og kun hvis denne variabel er blevet registreret, vil tjenesten blive genstartet.
Og selvfølgelig skal vi tilføje nginx-rollen i hoved-playbook'en.
Konfiguration af postgresql
Vi skal aktivere postgresql ved hjælp af systemd ligesom vi gjorde med nginx, og oprette en bruger, som vi vil bruge til at få adgang til databasen og selve databasen.
Lad os oprette 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 uddybe, hvordan man tilføjer variabler til inventory, det er allerede blevet gjort mange gange, ligesom syntaksen for modulerne postgresql_db og postgresql_user. Flere oplysninger findes i dokumentationen. Her er den mest interessante direktiv become_user: postgres. Faktisk har kun brugeren postgres som standard 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).
Måske skal du også tilføje en linje i pg_hba.conf for at åbne adgang for den nye bruger til databasen. Dette kan gøres på samme måde, som vi ændrede nginx-konfigurationen.
Og selvfølgelig skal du tilføje postgresql-rollen i hoved-playbook'en.
Installation af ruby via rbenv
Der er ikke moduler til rbenv i ansible, og det installeres ved at klone git-repositoriet. Derfor bliver denne opgave den mest usædvanlige. Lad os oprette en rolle til dette /ansible/roles/ruby_rbenv/main.yml og begynde at udfylde den:
# Install rbenv and ruby
- name: Install rbenv
become_user: "{{ user }}"
git: repo=https://github.com/rbenv/rbenv.git dest=~/.rbenvVi bruger igen direktivet become_user for at arbejde fra den bruger, vi har oprettet til dette formål. Da rbenv installeres i hans hjemmebibliotek og ikke globalt. Vi anvender også git-modulet til at klone repository'en ved at angive repo og dest.
Dernæst skal vi tilføje rbenv init til bashrc og også tilsætte rbenv til PATH. Til dette har vi lineinfile-modulet:
- name: Tilføj rbenv til PATH
become_user: "{{ user }}"
lineinfile:
path: ~\/\.bashrc
state: present
line: 'export PATH="${HOME}\/\.rbenv\/bin:${PATH}"'
- name: Tilføj rbenv init til bashrc
become_user: "{{ user }}"
lineinfile:
path: ~\/\.bashrc
state: present
line: 'eval "$(rbenv init -)"'Derefter skal vi installere ruby_build:
- name: Installer ruby-build
become_user: "{{ user }}"
git: repo=https:\/\/github.com\/rbenv\/ruby-build.git dest=~\/\.rbenv\/plugins\/ruby-buildOg endelig skal vi installere ruby. Dette gøres via rbenv, simpelthen med en bash-kommando:
- name: Installer ruby
become_user: "{{ user }}"
shell: |
export PATH="${HOME}\/\.rbenv\/bin:${PATH}"
eval "$(rbenv init -)"
rbenv install {{ ruby_version }}
args:
executable: \/bin\/bashVi angiver, hvilken kommando der skal udføres og med hvilket middel. Men her støder vi på, at ansible ikke udfører den kode, der findes i bashrc, før den udfører kommandoerne. Derfor skal rbenv defineres direkte i dette script.
Det næste problem er, at shell-kommandoen ikke har tilstand ud fra ansible's synspunkt. Det vil sige, at der ikke er nogen automatisk kontrol af, om denne version af ruby er installeret eller ej - vi kan gøre det selv:
- name: Installer 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\/bashOg vi skal installere bundler:
- name: Installer bundler
become_user: "{{ user }}"
shell: |
export PATH="${HOME}\/\.rbenv\/bin:${PATH}"
eval "$(rbenv init -)"
gem install bundlerOg igen tilføje vores rolle ruby_rbenv til hovedplaybook'en.
Delte filer.
Generelt kunne opsætningen her have været afsluttet. Derefter er det nødvendigt at køre capistrano, og det vil selv kopiere koden, oprette de nødvendige mapper og starte applikationen (hvis alt er korrekt konfigureret). Men ofte kræver capistrano yderligere konfigurationsfiler, såsom database.yml eller .env Disse kan kopieres på samme måde som filer og skabeloner til nginx. Der er kun en lille detalje. Før kopiering af filer skal der oprettes en mappestruktur for dem, noget som dette:
# Copy shared files for deploy
- name: Ensure shared dir
become_user: "{{ user }}"
file:
path: "{{ app_path }}/shared/config"
state: directoryVi angiver kun én mappe, og ansible vil automatisk oprette overordnede, hvis det er nødvendigt.
Ansible Vault
Vi er allerede stødt på, at der i variabler kan befinde sig hemmelige data såsom brugernes adgangskode. Hvis du har oprettet .env en fil til applikationen, og database.yml så må der være endnu flere sådanne kritiske data. Det ville være godt at skjule dem for uvedkommende. Til dette bruges .
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 inventory-filen: production.yml, staging.yml osv.).
I denne fil skal alle variabler, som skal krypteres, overføres 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_baseDerefter kan denne fil krypteres med kommandoen:
ansible-vault encrypt .\/vars\/all.ymlSelvfølgelig skal du ved kryptering indstille 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.
For at arbejde behøver du ikke at dekryptere filen. Du gemmer den i krypteret form og kører playbook med argumentet --ask-vault-pass. Ansible vil spørge om adgangskoden, indhente variablerne og udføre opgaverne. Alle data forbliver krypterede.
Den fulde kommando for flere grupper af værter og ansible vault vil se cirka sådan ud:
ansible-playbook -i inventory .\/playbook.yml -l "staging" --ask-vault-passOg jeg vil ikke give dig den fulde tekst af playbooks og roller, skriv dem selv. Fordi ansible er sådan en ting — hvis du ikke forstår, hvad der skal gøres, så gør den det heller ikke for dig.
Kilde: habr.com
