Serverkonfiguration zur Bereitstellung einer Rails-Anwendung mit Ansible

Vor nicht allzu langer Zeit musste ich mehrere Ansible-Playbooks schreiben, um einen Server für die Bereitstellung einer Rails-Anwendung vorzubereiten. Zu meiner Überraschung fand ich kein einfaches, schrittweises Handbuch. Ich wollte kein fremdes Playbook ohne Verständnis des Geschehens kopieren und musste schließlich die Dokumentation lesen, um alles selbst zusammenzustellen. Vielleicht kann ich jemandem helfen, diesen Prozess mit diesem Artikel zu beschleunigen.

Als Erstes muss man verstehen, dass Ansible Ihnen eine benutzerfreundliche Schnittstelle für die Ausführung einer im Voraus festgelegten Liste von Aktionen auf einem oder mehreren Remote-Servern über SSH bietet. Hier gibt es keine Magie; man kann kein Plugin installieren und eine sofortige Bereitstellung seiner Anwendung mit Docker, Monitoring und anderen Vorteilen erwarten. Um ein Playbook zu schreiben, müssen Sie wissen, was genau Sie tun möchten und wie Sie es umsetzen. Daher bin ich mit fertigen Playbooks von GitHub oder Artikeln, die besagen: "Kopieren und Ausführen - es wird funktionieren", nicht zufrieden.

Was benötigen wir?

Wie ich bereits gesagt habe, um ein Playbook zu schreiben, müssen Sie wissen, was Sie erreichen möchten und wie Sie es tun. Lassen Sie uns klären, was wir brauchen. Für eine Rails-Anwendung benötigen wir mehrere Systempakete: nginx, postgresql (redis usw.). Darüber hinaus benötigen wir Ruby in einer bestimmten Version. Am besten installiert man es über rbenv (rvm, asdf…). Alles als Root-Benutzer auszuführen, ist immer eine schlechte Idee, daher sollten wir einen separaten Benutzer erstellen und ihm die entsprechenden Berechtigungen zuweisen. Danach müssen wir unseren Code auf den Server hochladen und die Konfigurationen für nginx, postgres usw. kopieren und all diese Dienste starten.

Die Reihenfolge der Schritte sieht folgendermaßen aus:

  1. Einloggen als Root
  2. Systempakete installieren
  3. Neuen Benutzer erstellen, Berechtigungen und SSH-Schlüssel einrichten
  4. Systempakete (nginx usw.) einrichten und starten
  5. Benutzer in der Datenbank erstellen (kann auch gleich die Datenbank erstellt werden)
  6. Mit dem neuen Benutzer einloggen
  7. rbenv und Ruby installieren
  8. Bundler installieren
  9. Code der Anwendung hochladen
  10. Puma-Server starten

Die letzten Schritte können mit Capistrano durchgeführt werden; zumindest kann es von Haus aus den Code in die Release-Verzeichnisse kopieren, den Release durch einen Symlink bei erfolgreicher Bereitstellung umschalten, Konfigurationen aus dem Shared-Verzeichnis kopieren, Puma neu starten usw. All dies kann auch mit Ansible gemacht werden, aber warum?

Dateistruktur

Ansible hat eine strikte Ordnerstruktur für all seine Dateien, daher ist es am besten, alles in einem separaten Verzeichnis zu halten. Dabei ist es nicht so wichtig, ob es sich im Rails-Anwendungsbereich oder separat befindet. Man kann die Dateien in einem separaten Git-Repository speichern. Für mich persönlich war es am einfachsten, ein Verzeichnis namens ansible im /config-Verzeichnis der Rails-Anwendung zu erstellen und alles in einem Repository zu speichern.

Einfaches Playbook

Ein Playbook ist eine yml-Datei, in der mit einer speziellen Syntax beschrieben wird, was und wie Ansible etwas tun soll. Lassen Sie uns unser erstes Playbook erstellen, das nichts tut:

---
- name: Einfaches Playbook
  hosts: all

Hier sagen wir einfach, dass unser Playbook den Namen hat Einfaches Playbook und dass der Inhalt auf allen Hosts ausgeführt werden soll. Wir können es im /ansible-Verzeichnis unter dem Namen speichern playbook.yml und versuchen, es auszuführen:

ansible-playbook ./playbook.yml

PLAY [Einfaches Playbook] ************************************************************************************************************************************
skipping: keine Hosts gefunden

Ansible sagt, dass es keine Hosts kennt, die mit der Liste all übereinstimmen. Sie müssen in einer speziellen Inventory-Datei aufgeführt werden..

Lassen Sie uns diese im gleichen ansible-Verzeichnis erstellen:

123.123.123.123

So geben wir einfach den Host an (idealerweise den Host Ihres VPS zum Testen, oder Sie können localhost eingeben) und speichern ihn unter dem Namen inventory.
Sie können versuchen, Ansible mit der Inventory-Datei auszuführen:

ansible-playbook ./playbook.yml -i inventory
PLAY [Einfaches Playbook] ************************************************************************************************************************************

TASK [Fakten sammeln] ************************************************************************************************************************************

PLAY RETURNAUSFAZIT ************************************************************************************************************************************

Wenn Sie SSH-Zugriff auf den angegebenen Host haben, wird Ansible eine Verbindung herstellen und Informationen über das entfernte System sammeln. (standard TASK [Fakten sammeln]) danach wird eine kurze Zusammenfassung der Ausführung bereitgestellt (PLAY RETURNAUSFAZIT).

Standardmäßig wird der Benutzername verwendet, mit dem Sie sich im System angemeldet haben. Auf dem Host wird dieser wahrscheinlich nicht vorhanden sein. Im Playbook können Sie den Benutzer angeben, der für die Verbindung verwendet werden soll, indem Sie die Direktive remote_user verwenden. Oftmals benötigen Sie auch keine Informationen über das entfernte System und sollten keine Zeit mit deren Sammlung vergeuden. Diese Aufgabe kann ebenfalls deaktiviert werden:

---
- name: Einfaches Playbook
  hosts: all
  remote_user: root
  become: true
  gather_facts: no

Versuchen Sie, das Playbook erneut zu starten und sicherzustellen, dass die Verbindung funktioniert. (Wenn Sie den Benutzer root angegeben haben, müssen Sie auch die Direktive become: true angeben, um erhöhte Rechte zu erhalten. Wie in der Dokumentation beschrieben: become auf ‚true‘/‘yes‘ setzen, um die Berechtigungseskalation zu aktivieren. obwohl es nicht ganz klar ist, warum).

Möglicherweise erhalten Sie einen Fehler, weil ansible den Python-Interpreter nicht bestimmen kann. In diesem Fall können Sie ihn manuell angeben:

ansible_python_interpreter: /usr/bin/python3 

wo Ihr Python installiert ist, können Sie mit dem Befehl herausfinden whereis python.

Installation von Systempaketen

Im Standardpaket von Ansible sind viele Module zur Arbeit mit verschiedenen Systempaketen enthalten, wodurch wir nicht für jeden Anlass Bash-Skripte schreiben müssen. Jetzt benötigen wir eines dieser Module, um das System zu aktualisieren und Systempakete zu installieren. Auf meinem VPS läuft Ubuntu Linux, entsprechend verwende ich apt-get und das Modul dafür. Wenn Sie ein anderes Betriebssystem verwenden, benötigen Sie möglicherweise ein anderes Modul (denken Sie daran, dass ich zu Beginn gesagt habe, dass Sie im Voraus wissen müssen, was und wie wir es machen). Der Syntax wird jedoch wahrscheinlich ähnlich sein.

Ergänzen wir unser Playbook mit den ersten Aufgaben:

---
- name: Einfaches Playbook
  hosts: all
  remote_user: root
  become: true
  gather_facts: no

  tasks:
    - name: System aktualisieren
      apt: update_cache=yes
    - name: Systemabhängigkeiten installieren
      apt:
        name: git,nginx,redis,postgresql,postgresql-contrib
        state: present

Eine Aufgabe ist genau das, was ansible auf den entfernten Servern ausführen wird. Wir geben der Aufgabe einen Namen, um ihre Ausführung im Protokoll zu verfolgen. Und wir beschreiben, mit der Syntax des spezifischen Moduls, was es tun soll. In diesem Fall apt: update_cache=yes – sagt, dass die Pakete des Systems mit dem Modul apt aktualisiert werden sollen. Der zweite Befehl ist etwas komplizierter. Wir übergeben dem Modul apt eine Liste von Paketen und sagen, dass deren state installiert werden sollen present, d.h. wir verlangen, dass diese Pakete installiert werden. Auf ähnliche Weise können wir angeben, dass sie gelöscht oder aktualisiert werden sollen, indem wir einfach stateändern. Beachten Sie, dass wir für die Arbeit von rails mit postgresql das Paket postgresql-contrib benötigen, das wir jetzt installieren. Auch dies muss man wissen und tun, ansible wird dies nicht von selbst erledigen.

Versuchen Sie, das Playbook erneut auszuführen und zu überprüfen, ob die Pakete installiert werden.

Erstellen neuer Benutzer.

Für die Arbeit mit Benutzern gibt es bei Ansible ebenfalls ein Modul – user. Lassen Sie uns eine weitere Aufgabe hinzufügen (ich habe die bereits bekannten Teile des Playbooks in Kommentaren verborgen, um es nicht jedes Mal vollständig kopieren zu müssen):

---
- name: Einfaches Playbook
  # ...
  tasks:
    # ...
    - name: Neuen Benutzer hinzufügen
      user:
        name: my_user
        shell: /bin/bash
        password: "{{ 123qweasd | password_hash('sha512') }}"

Wir erstellen einen neuen Benutzer, setzen ihm eine Shell und ein Passwort. Und schon sehen wir uns mehreren Problemen gegenüber. Was ist, wenn die Benutzernamen für verschiedene Hosts unterschiedlich sein müssen? Außerdem ist es eine sehr schlechte Idee, das Passwort im Klartext im Playbook zu speichern. Zunächst werden wir den Benutzernamen und das Passwort in Variablen auslagern, und gegen Ende des Artikels zeige ich, wie das Passwort verschlüsselt werden kann.

---
- name: Einfaches Playbook
  # ...
  tasks:
    # ...
    - name: Neuen Benutzer hinzufügen
      user:
        name: "{{ user }}"
        shell: /bin/bash
        password: "{{ user_password | password_hash('sha512') }}"

Mit den doppelten geschweiften Klammern werden in Playbooks Variablen gesetzt.

Die Werte der Variablen geben wir in der Inventardatei an:

123.123.123.123

[all:vars]
user=my_user
user_password=123qweasd

Beachten Sie die Direktive [all:vars] – sie bedeutet, dass der folgende Textblock Variablen (vars) enthält, die für alle Hosts (all) angewendet werden.

Auch die Konstruktion "{{ user_password | password_hash('sha512') }}". Das Problem ist, dass Ansible den Benutzer nicht über user_add hinzufügt, wie Sie es manuell tun würden. Stattdessen speichert es alle Daten direkt, weshalb wir das Passwort auch vorher in einen Hash umwandeln müssen, was dieser Befehl tut.

Lassen Sie uns unseren Benutzer zur Sudo-Gruppe hinzufügen. Bevor wir das tun, müssen wir jedoch sicherstellen, dass diese Gruppe vorhanden ist, denn niemand wird das für uns erledigen:

---
- name: Einfaches Playbook
  # ...
  tasks:
    # ...
    - name: Sicherstellen, dass eine 'sudo'-Gruppe existiert
      group:
        name: sudo
        state: present
    - name: Neuen Benutzer hinzufügen
      user:
        name: "{{ user }}"
        shell: /bin/bash
        password: "{{ user_password | password_hash('sha512') }}"
        groups: "sudo"

Das ist alles ziemlich einfach, wir haben auch ein Modul group zur Erstellung von Gruppen, dessen Syntax sehr ähnlich wie bei apt ist. Danach müssen wir nur noch diese Gruppe dem Benutzer zuweisen (groups: "sudo").
Es ist auch nützlich, diesem Benutzer einen SSH-Schlüssel hinzuzufügen, damit wir uns ohne Passwort unter ihm einloggen können:

---
- name: Einfaches Playbook
  # ...
  Aufgaben:
    # ...
    - name: Eine 'sudo'-Gruppe sicherstellen
      gruppe:
        name: sudo
        state: present
    - name: Einen neuen Benutzer hinzufügen
      benutzer:
        name: "{{ user }}"
        shell: /bin/bash
        passwort: "{{ user_password | password_hash('sha512') }}"
        gruppen: "sudo"
    - name: SSH-Schlüssel bereitstellen
      authorized_key:
        benutzer: "{{ user }}"
        schlüssel: "{{ lookup('file', '~/.ssh/id_rsa.pub') }}"
        state: present

In diesem Fall ist die Konstruktion interessant "{{ lookup('file', '~/.ssh/id_rsa.pub') }}" — sie kopiert den Inhalt der Datei id_rsa.pub (bei Ihnen könnte der Name anders sein), also den öffentlichen Teil des SSH-Schlüssels und lädt ihn in die Liste der autorisierten Schlüssel für den Benutzer auf dem Server hoch.

Rollen

Alle drei Aufgaben zur Benutzererstellung können leicht einer Gruppe von Aufgaben zugeordnet werden, und es wäre sinnvoll, diese Gruppe separat vom Haupt-Playbook zu speichern, damit es nicht zu umfangreich wird. Dafür gibt es in Ansible Rollen.
Gemäß der zu Beginn angegebenen Verzeichnisstruktur sollten Rollen in ein separates Verzeichnis 'roles' gelegt werden, wobei jedes Rolle ein eigenes Verzeichnis mit einem ähnlichen Namen hat, innerhalb des Verzeichnisses 'tasks', 'files', 'templates', usw.
Lassen Sie uns die Verzeichnisstruktur erstellen: ./ansible/roles/user/tasks/main.yml (main ist die Hauptdatei, die geladen und beim Einfügen der Rolle in das Playbook ausgeführt wird; darin können andere Dateien der Rolle eingebunden werden). Jetzt können wir alle Aufgaben, die sich auf den Benutzer beziehen, in diese Datei verschieben:

# 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

Im Haupt-Playbook muss angegeben werden, dass die Rolle 'user' verwendet werden soll:

---
- name: Einfaches Playbook
  hosts: all
  remote_user: root
  gather_facts: no

  Aufgaben:
    - name: System aktualisieren
      apt: update_cache=yes
    - name: Systemabhängigkeiten installieren
      apt:
        name: git,nginx,redis,postgresql,postgresql-contrib
        state: present

  Rollen:
    - user

Es könnte auch sinnvoll sein, das System vor allen anderen Aufgaben zu aktualisieren, dafür könnte man den Block umbenennen tasks in dem sie definiert sind zu pre_tasks.

Konfiguration von nginx

Nginx sollte bereits installiert sein, es muss konfiguriert und gestartet werden. Lassen Sie uns das sofort in der Rolle erledigen. Wir erstellen die Verzeichnisstruktur:

- ansible
  - roles
    - nginx
      - files
      - tasks
        - main.yml
      - templates

Jetzt benötigen wir Dateien und Vorlagen. Der Unterschied zwischen ihnen besteht darin, dass Ansible Dateien direkt kopiert, wie sie sind. Vorlagen müssen die Erweiterung j2 haben, und in ihnen können Variablenwerte mit denselben doppelten geschweiften Klammern verwendet werden.

Lassen Sie uns Nginx in die main.yml Datei einfügen. Dazu haben wir das Modul systemd:

# Copy nginx configs and start it
- name: enable service nginx and start
  systemd:
    name: nginx
    state: started
    enabled: yes

Hier sagen wir nicht nur, dass Nginx gestartet werden soll (das heißt, wir starten es), sondern auch, dass es aktiviert sein soll.
Jetzt kopieren wir die Konfigurationsdateien:

# 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'

Wir erstellen die Hauptkonfigurationsdatei für nginx (entweder direkt vom Server nehmen oder selbst schreiben). Außerdem erstellen wir die Konfigurationsdatei für unsere Anwendung im Verzeichnis sites_available (das ist nicht zwingend erforderlich, aber nützlich). Im ersten Fall verwenden wir das Modul copy, um die Dateien zu kopieren (die Datei sollte liegen in /ansible/roles/nginx/files/nginx.conf). Im zweiten Fall kopieren wir die Vorlage und setzen die Variablenwerte ein. Die Vorlage sollte liegen in /ansible/roles/nginx/templates/my_app.j2). Und sie könnte etwa so aussehen:

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 }};
  ....
}

Beachten Sie die Platzhalter {{ app_name }}, {{ app_path }}, {{ server_name }}, {{ inventory_hostname }} — das sind alles Variablen, deren Werte Ansible vor dem Kopieren in die Vorlage einfügt. Das ist nützlich, wenn Sie das Playbook für verschiedene Gruppen von Hosts verwenden. Zum Beispiel können wir unsere Inventardatei erweitern:

[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

Wenn wir jetzt unser Playbook ausführen, wird es die angegebenen Aufgaben für beide Hosts ausführen. Für den Staging-Host werden die Variablen jedoch von der Produktion abweichen, und das nicht nur in Rollen und Playbooks, sondern auch in den nginx-Konfigurationen. {{ inventory_hostname }} Es ist nicht nötig, dies in der Inventardatei anzugeben — das ist eine spezielle Ansible-Variable und dort wird der Host gespeichert, für den das Playbook gerade ausgeführt wird.
Wenn Sie eine Inventardatei für mehrere Hosts haben möchten, aber nur für eine Gruppe ausführen wollen, können Sie dies mit dem folgenden Befehl tun:

ansible-playbook -i inventory .\/playbook.yml -l "staging"

Eine andere Möglichkeit besteht darin, separate Inventardateien für verschiedene Gruppen zu haben. Oder Sie können beide Ansätze kombinieren, wenn Sie viele verschiedene Hosts haben.

Kehren wir zur Konfiguration von nginx zurück. Nach dem Kopieren der Konfigurationsdateien müssen wir einen Symlink in sites_enabled zu my_app.conf aus sites_available erstellen. Und nginx neu starten.

... # alter Code in mail.yml

- name: Symlink zu sites-enabled erstellen
  file:
    src: \/etc\/nginx\/sites-available\/my_app.conf
    dest: \/etc\/nginx\/sites-enabled\/my_app.conf
    state: link

- name: nginx neu starten
  service:
    name: nginx
    state: restarted

Hier ist alles einfach – wieder die ansible-Module mit einer ziemlich standardisierten Syntax. Aber es gibt einen Punkt. Es macht keinen Sinn, nginx jedes Mal neu zu starten. Ihnen ist sicher aufgefallen, dass wir keine Befehle schreiben wie: „Mach das so“, sondern die Syntax eher so aussieht: „Diese Entität sollte diesen Zustand haben“. Und so funktioniert ansible in der Regel. Wenn die Gruppe bereits existiert oder das Systempaket bereits installiert ist, wird ansible dies überprüfen und die Aufgabe überspringen. Auch Dateien werden nicht kopiert, wenn sie vollständig mit dem übereinstimmen, was bereits auf dem Server vorhanden ist. Wir können dies ausnutzen und nginx nur dann neu starten, wenn sich die Konfigurationsdateien geändert haben. Dafür gibt es die Direktive 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.changed

Wenn eine der Konfigurationsdateien geändert wird, wird die Datei kopiert und eine Variable registriert. restart_nginx. Und nur wenn diese Variable registriert wurde, wird der Dienst neu gestartet.

Und natürlich muss die Rolle nginx dem Haupt-Playbook hinzugefügt werden.

PostgreSQL-Konfiguration

Wir müssen PostgreSQL mit systemd aktivieren, genau wie wir es mit nginx gemacht haben, und auch einen Benutzer erstellen, den wir für den Zugriff auf die Datenbank verwenden werden, sowie die Datenbank selbst.
Lesen wir eine Rolle erstellen /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 }}"

Ich werde nicht näher darauf eingehen, wie man Variablen im Inventory hinzufügt, das wurde bereits oft genug gemacht, ebenso wie die Syntax der Module postgresql_db und postgresql_user. Mehr Informationen finden Sie in der Dokumentation. Die interessanteste Direktive hier ist: become_user: postgres. Der Grund dafür ist, dass der Zugriff auf die PostgreSQL-Datenbank standardmäßig nur für den Benutzer postgres und nur lokal verfügbar ist. Diese Direktive ermöglicht es uns, Befehle im Namen dieses Benutzers auszuführen (vorausgesetzt, wir haben Zugang).
Möglicherweise müssen Sie auch eine Zeile in pg_hba.conf hinzufügen, um dem neuen Benutzer Zugriff auf die Datenbank zu ermöglichen. Das können Sie genauso tun, wie wir die nginx-Konfiguration geändert haben.

Natürlich müssen wir auch die Rolle postgresql dem Haupt-Playbook hinzufügen.

Installation von Ruby über rbenv

Es gibt keine Module in ansible für die Arbeit mit rbenv, da es durch das Klonen des Git-Repositories installiert wird. Daher wird diese Aufgabe die am wenigsten standardisierte. Lassen Sie uns dafür eine Rolle erstellen /ansible/roles/ruby_rbenv/main.yml und anfangen, sie auszufüllen:

# Install rbenv and ruby
- name: Install rbenv
  become_user: "{{ user }}"
  git: repo=https://github.com/rbenv/rbenv.git dest=~/.rbenv

Wir verwenden wieder die Direktive become_user, um von dem von uns dafür erstellten Benutzer aus zu arbeiten. Da rbenv im Home-Verzeichnis dieses Benutzers und nicht global installiert wird. Außerdem verwenden wir das Modul git, um das Repository anzugeben, wobei wir repo und dest angeben.

Als nächstes müssen wir rbenv init in der bashrc eintragen und rbenv zugleich in den PATH hinzufügen. Dazu haben wir das Modul lineinfile:

- name: rbenv zum PATH hinzufügen
  become_user: "{{ user }}"
  lineinfile:
    path: ~\/bashrc
    state: present
    line: 'export PATH="${HOME}\/\.rbenv\/bin:${PATH}"'

- name: rbenv init in die bashrc eintragen
  become_user: "{{ user }}"
  lineinfile:
    path: ~\/bashrc
    state: present
    line: 'eval "$(rbenv init -)"'

Danach muss ruby_build installiert werden:

- name: ruby-build installieren
  become_user: "{{ user }}"
  git: repo=https:\/\/github.com\/rbenv\/ruby-build.git dest=~\/\.rbenv\/plugins\/ruby-build

Und schließlich ruby installieren. Das erfolgt über rbenv, also einfach mit dem Bash-Befehl:

- name: ruby installieren
  become_user: "{{ user }}"
  shell: |
    export PATH="${HOME}\/\.rbenv\/bin:${PATH}"
    eval "$(rbenv init -)"
    rbenv install {{ ruby_version }}
  args:
    executable: \/bin\/bash

Wir geben an, welchen Befehl wir ausführen wollen und womit. Allerdings stoßen wir hier auf das Problem, dass Ansible keinen Code ausführt, der in der bashrc enthalten ist, bevor die Befehle ausgeführt werden. Das bedeutet, dass rbenv direkt in diesem Skript definiert werden muss.

Das nächste Problem hängt damit zusammen, dass der Shell-Befehl aus der Sicht von Ansible keinen Zustand hat. Das bedeutet, es wird keine automatische Überprüfung vorgenommen, ob diese ruby-Version installiert ist oder nicht — das müssen wir selbst erledigen:

- name: ruby installieren
  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

Und nun bleibt nur noch, bundler zu installieren:

- name: bundler installieren
  become_user: "{{ user }}"
  shell: |
    export PATH="${HOME}\/\.rbenv\/bin:${PATH}"
    eval "$(rbenv init -)"
    gem install bundler

Und wieder müssen wir unsere Rolle ruby_rbenv im Haupt-Playbook hinzufügen.

Geteilte Dateien.

Im Allgemeinen könnte man die Konfiguration hier beenden. Danach bleibt es, capistrano zu starten, und es kopiert den Code von selbst, erstellt die notwendigen Verzeichnisse und startet die Anwendung (sofern alles korrekt eingerichtet ist). Allerdings benötigt capistrano oftmals zusätzliche Konfigurationsdateien, wie zum Beispiel database.yml oder .env Diese können genau wie die Dateien und Vorlagen für nginx kopiert werden. Es gibt nur einen kleinen Unterschied. Vor dem Kopieren der Dateien muss die Verzeichnisstruktur dafür erstellt werden, etwa so:

# Copy shared files for deploy
- name: Ensure shared dir
  become_user: "{{ user }}"
  file:
    path: "{{ app_path }}/shared/config"
    state: directory

wir geben nur ein Verzeichnis an, und Ansible erstellt automatisch die übergeordneten, wenn nötig.

Ansible Vault

Wir haben bereits festgestellt, dass in Variablen geheime Daten wie das Benutzerpasswort gespeichert werden können. Wenn Sie eine .env Datei für die Anwendung erstellt haben, dann database.yml gibt es dort sicherlich noch mehr solcher kritischen Daten. Es wäre gut, sie vor neugierigen Blicken zu verbergen. Dazu verwenden wir ansible vault.

Lassen Sie uns eine Datei für Variablen erstellen /ansible/vars/all.yml (hier können Sie verschiedene Dateien für verschiedene Gruppen von Hosts erstellen, genau wie in der Inventory-Datei: production.yml, staging.yml, usw.).
In diese Datei müssen alle Variablen übertragen werden, die verschlüsselt werden sollen, unter Verwendung der Standard-yml-Syntax:

# 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

Anschließend kann diese Datei mit folgendem Befehl verschlüsselt werden:

ansible-vault encrypt ./vars/all.yml

Natürlich müssen Sie beim Verschlüsseln ein Passwort für die Entschlüsselung festlegen. Sie können prüfen, was nach dem Aufruf dieses Befehls in der Datei enthalten sein wird.

Mit ansible-vault decrypt Die Datei kann entschlüsselt, geändert und dann erneut verschlüsselt werden.

Für die Arbeit müssen Sie die Datei nicht entschlüsseln. Sie speichern sie in verschlüsselter Form und führen das Playbook mit dem Argument --ask-vault-passaus. Ansible fragt nach dem Passwort, greift auf die Variablen zu und führt die Aufgaben aus. Alle Daten bleiben verschlüsselt.

Das gesamte Kommando für mehrere Gruppen von Hosts und ansible vault würde ungefähr so aussehen:

ansible-playbook -i inventory ./playbook.yml -l "staging" --ask-vault-pass

Ich werde Ihnen den kompletten Text der Playbooks und Rollen nicht geben, schreiben Sie selbst. Denn ansible ist so – wenn Sie nicht verstehen, was zu tun ist, wird es das auch nicht für Sie tun.

Quelle: habr.com

60GB SSD 8Gb DDR4