配置服務器以使用 Ansible 部署 Rails 應用程序

不久前,我需要寫幾台 Ansible playbook 來準備部署 Rails 應用程式的伺服器。 而且,令人驚訝的是,我沒有找到簡單的逐步手冊。 我不想在不了解發生了什麼的情況下複製別人的劇本,最後我不得不閱讀文檔,自己收集所有內容。 也許我可以在本文的幫助下幫助某人加快這個過程。

首先要了解的是,ansible 為您提供了一個方便的介面,可以透過 SSH 在遠端伺服器上執行預先定義的操作清單。 這裡沒有魔法,您無法安裝插件並使用 docker、監控和其他開箱即用的好東西來零停機部署您的應用程式。 為了寫一本劇本,你必須知道你到底想做什麼、如何做。 這就是為什麼我對 GitHub 上現成的劇本或諸如“複製並運行,它會起作用”之類的文章不滿意。

我們需要什麼?

正如我已經說過的,為了寫一本劇本,你需要知道你想做什麼以及如何做。 讓我們決定我們需要什麼。 對於 Rails 應用程序,我們需要幾個系統套件:nginx、postgresql(redis 等)。 此外,我們還需要特定版本的 ruby​​。 最好透過 rbenv(rvm、asdf...)安裝。 以 root 使用者身分執行所有這些總是一個壞主意,因此您需要建立一個單獨的使用者並配置他的權限。 之後,您需要將我們的程式碼上傳到伺服器,複製 nginx、postgres 等的配置並啟動所有這些服務。

結果,動作順序如下:

  1. 以 root 身分登入
  2. 安裝系統套件
  3. 建立新用戶,配置權限,ssh金鑰
  4. 配置系統包(nginx等)並運行它們
  5. 我們在資料庫中建立使用者(可以立即建立資料庫)
  6. 以新使用者登入
  7. 安裝 rbenv 和 ruby
  8. 安裝捆綁器
  9. 上傳應用程式程式碼
  10. 啟動 Puma 伺服器

此外,最後階段可以使用 capistrano 完成,至少開箱即用,它可以將程式碼複製到發布目錄中,在成功部署後使用符號連結切換版本,從共享目錄複製配置,重新啟動 puma 等。 所有這些都可以使用 Ansible 來完成,但為什麼呢?

文件結構

Ansible 有嚴格的 文件結構 對於所有文件,因此最好將其全部保存在單獨的目錄中。 此外,它是在 Rails 應用程式本身還是單獨存在並不那麼重要。 您可以將檔案儲存在單獨的 git 儲存庫中。 就我個人而言,我發現在 Rails 應用程式的 /config 目錄中建立一個 ansible 目錄並將所有內容儲存在一個儲存庫中是最方便的。

簡單的劇本

Playbook 是一個 yml 文件,它使用特殊語法來描述 Ansible 應該做什麼以及如何做。 讓我們創建第一個不執行任何操作的劇本:

---
- name: Simple playbook
  hosts: all

這裡我們簡單地說我們的劇本叫做 Simple Playbook 並且它的內容應該為所有主機執行。 我們可以將其保存在 /ansible 目錄中,名稱為 playbook.yml 並嘗試運行:

ansible-playbook ./playbook.yml

PLAY [Simple Playbook] ************************************************************************************************************************************
skipping: no hosts matched

Ansible 表示它不知道任何與所有清單相符的主機。 它們必須列在特殊的 庫存文件.

讓我們在同一個 ansible 目錄中建立它:

123.123.123.123

這就是我們簡單地指定主機(最好是我們用於測試的VPS的主機,或者你可以註冊localhost)並將其保存在名稱下 inventory.
您可以嘗試使用清單檔案執行 ansible:

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

TASK [Gathering Facts] ************************************************************************************************************************************

PLAY RECAP ************************************************************************************************************************************

如果您可以透過 ssh 存取指定主機,那麼 ansible 將連接並收集有關遠端系統的資訊。 (預設任務[收集事實])之後它將給出有關執行情況的簡短報告(重述)。

預設情況下,連線使用您登入系統時使用的使用者名稱。 它很可能不在主機上。 在 playbook 檔案中,您可以使用remote_user 指令指定用於連線的使用者。 此外,有關遠端系統的資訊通常對您來說可能是不必要的,您不應該浪費時間收集它。 也可以停用此任務:

---
- name: Simple playbook
  hosts: all
  remote_user: root
  become: true
  gather_facts: no

嘗試再次執行 playbook 並確保連線正常。 (如果您指定了root用戶,那麼您還需要指定become: true指令才能獲得提升的權限。如文件所述: become set to ‘true’/’yes’ to activate privilege escalation. 儘管尚不完全清楚原因)。

也許你會收到由於ansible無法確定Python解釋器而導致的錯誤,那麼你可以手動指定它:

ansible_python_interpreter: /usr/bin/python3 

您可以使用以下命令找出 python 的位置 whereis python.

安裝系統套件

Ansible 的標準發行版包含許多用於處理各種系統套件的模組,因此我們不必出於任何原因編寫 bash 腳本。 現在我們需要這些模組之一來更新系統並安裝系統軟體包。 我的 VPS 上有 Ubuntu Linux,因此要安裝我使用的軟體包 apt-get и 它的模組。 如果您使用不同的作業系統,那麼您可能需要不同的模組(記住,我在一開始就說過,我們需要提前知道我們將做什麼以及如何做)。 然而,語法很可能是相似的。

讓我們用第一個任務來補充我們的劇本:

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

任務正是 Ansible 將在遠端伺服器上執行的任務。 我們為任務命名,以便我們可以在日誌中追蹤其執行情況。 我們使用特定模組的語法來描述它需要做什麼。 在這種情況下 apt: update_cache=yes - 表示使用 apt 模組更新系統軟體包。 第二個指令稍微複雜一些。 我們將包列表傳遞給 apt 模組並說它們是 state 應該成為 present,也就是我們說的安裝這些套件。 以類似的方式,我們可以告訴他們刪除它們,或者透過簡單地更改來更新它們 state。 請注意,為了讓 Rails 能夠與 postgresql 一起工作,我們需要 postgresql-contrib 包,我們現在正在安裝它。 再次強調,您需要知道並做到這一點;ansible 本身無法做到這一點。

嘗試再次執行 playbook 並檢查軟體包是否已安裝。

建立新使用者。

為了與用戶合作,Ansible 還有一個模組 - 用戶。 讓我們再增加一項任務(我將劇本中已知的部分隱藏在註釋後面,以免每次都完全複製):

---
- name: Simple playbook
  # ...
  tasks:
    # ...
    - name: Add a new user
      user:
        name: my_user
        shell: /bin/bash
        password: "{{ 123qweasd | password_hash('sha512') }}"

我們建立一個新用戶,為其設定一個 schell 和密碼。 然後我們遇到了幾個問題。 如果不同主機的使用者名稱需要不同怎麼辦? 並且以明文形式將密碼儲存在劇本中是一個非常糟糕的主意。 首先,讓我們將使用者名稱和密碼放入變數中,在文章末尾,我將展示如何加密密碼。

---
- name: Simple playbook
  # ...
  tasks:
    # ...
    - name: Add a new user
      user:
        name: "{{ user }}"
        shell: /bin/bash
        password: "{{ user_password | password_hash('sha512') }}"

變數在劇本中使用雙花括號設定。

我們將在庫存文件中指示變數的值:

123.123.123.123

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

請注意指令 [all:vars] - 它表示下一個文字區塊是變數(vars)並且它們適用於所有主機(all)。

設計也很有趣 "{{ user_password | password_hash('sha512') }}"。 問題是 ansible 不透過安裝用戶 user_add 就像你手動做的一樣。 而且它直接保存了所有數據,這就是為什麼我們還必須提前將密碼轉換為哈希值,這就是這個命令的作用。

讓我們將用戶新增到 sudo 群組。 然而,在此之前我們需要確保這樣一個組的存在,因為沒有人會為我們做這件事:

---
- 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"

一切都很簡單,我們還有一個用於建立群組的群組模組,其語法與 apt 非常相似。 然後將這個群組註冊給用戶就夠了(groups: "sudo").
向該用戶添加 ssh 密鑰也很有用,這樣我們就可以使用它而無需密碼登入:

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

在這種情況下,設計很有趣 "{{ lookup('file', '~/.ssh/id_rsa.pub') }}" — 它複製 id_rsa.pub 檔案的內容(您的名字可能不同),即 ssh 金鑰的公共部分,並將其上傳到伺服器上使用者的授權金鑰清單。

角色

用於創建用途的所有三個任務都可以輕鬆地分為一組任務,最好將這組任務與主劇本分開存儲,這樣它就不會變得太大。 為此,Ansible 已 角色.
根據一開始所示的檔案結構,角色必須放置在單獨的角色目錄中,每個角色都有單獨的同名目錄,裡面有任務、檔案、範本等目錄
讓我們建立一個文件結構: ./ansible/roles/user/tasks/main.yml (main 是角色連接到 playbook 時將載入並執行的主檔案;其他角色檔案可以連接到它)。 現在您可以將與使用者相關的所有任務傳輸到此文件:

# 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

在主劇本中,您必須指定使用使用者角色:

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

此外,在所有其他任務之前更新系統可能是有意義的;為此,您可以重新命名該區塊 tasks 其中它們被定義在 pre_tasks.

設置 nginx

我們應該已經安裝了 Nginx;我們需要設定它並運行它。 讓我們立即在角色中進行操作。 讓我們建立一個文件結構:

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

現在我們需要文件和模板。 它們之間的區別在於,ansible 直接按原樣複製文件。 並且模板必須具有 j2 副檔名,並且它們可以使用相同的雙花括號來使用變數值。

讓我們啟用 nginx main.yml 文件。 為此,我們有一個 systemd 模組:

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

這裡我們不僅說nginx必須啟動(即我們啟動它),而且我們立即說它必須啟用。
現在讓我們複製設定檔:

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

我們建立主要的 nginx 設定檔(您可以直接從伺服器取得,也可以自己寫)。 還有 site_available 目錄中我們應用程式的設定檔(這不是必需的,但很有用)。 第一種情況,我們使用copy模組來複製檔案(檔案必須在 /ansible/roles/nginx/files/nginx.conf)。 在第二個中,我們複製模板,替換變數的值。 模板應該在 /ansible/roles/nginx/templates/my_app.j2)。 它可能看起來像這樣:

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

注意插入物 {{ app_name }}, {{ app_path }}, {{ server_name }}, {{ inventory_hostname }} — 這些是 Ansible 在複製之前將其值替換到範本中的所有變數。 如果您對不同的主機組使用 playbook,這非常有用。 例如,我們可以新增庫存文件:

[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

如果我們現在啟動我們的劇本,它將為兩台主機執行指定的任務。 但同時,對於登台主機,變數將與生產主機不同,不僅在角色和劇本中,而且在 nginx 配置中也是如此。 {{ inventory_hostname }} 不需要在庫存文件中指定 - 這 特殊的 ansible 變數 目前正在運行劇本的主機儲存在那裡。
如果您想要為多個主機建立一個清單文件,但只為一組運行,可以使用以下命令來完成:

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

另一種選擇是為不同的群組提供單獨的清單檔案。 或者,如果您有許多不同的主機,您可以結合這兩種方法。

讓我們回到設定 nginx。 複製設定檔後,我們需要在sitest_enabled 中建立一個從sites_available 到my_app.conf 的符號連結。 並重啟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

這裡一切都很簡單——同樣具有相當標準語法的ansible模組。 但有一點。 每次重新啟動 nginx 是沒有意義的。 你有沒有註意到,我們不會寫這樣的命令:“像這樣做”,語法看起來更像是“這應該有這個狀態”。 大多數情況下,這正是 ansible 的工作原理。 如果該群組已經存在,或者係統軟體包已經安裝,那麼 ansible 將檢查這一點並跳過該任務。 此外,如果檔案與伺服器上已有的檔案完全匹配,則不會複製檔案。 我們可以利用這一點,僅在設定檔發生變更時重新啟動 nginx。 為此有一個暫存器指令:

# 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

如果其中一個設定檔發生更改,則會製作一份副本並註冊該變量 restart_nginx。 而只有當這個變數被註冊後,服務才會重新啟動。

當然,您需要將 nginx 角色新增至主 playbook 。

設定 postgresql

我們需要像使用 nginx 一樣使用 systemd 啟用 postgresql,並建立一個用於存取資料庫和資料庫本身的使用者。
讓我們創建一個角色 /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 }}"

我不會描述如何將變數新增到清單中,這已經做過很多次了,以及 postgresql_db 和 postgresql_user 模組的語法。 更多資訊可以在文件中找到。 這裡最有趣的指令是 become_user: postgres。 事實上,預設情況下,只有 postgres 使用者有權存取 postgresql 資料庫,而且只能在本地存取。 該指令允許我們代表該使用者執行命令(當然,如果我們有存取權限的話)。
另外,您可能需要在 pg_hba.conf 中新增一行以允許新使用者存取資料庫。 這可以透過與更改 nginx 配置相同的方式來完成。

當然,您需要將 postgresql 角色新增到主 playbook 中。

透過rbenv安裝ruby

Ansible 沒有用於使用 rbenv 的模組,但它是透過複製 git 儲存庫來安裝的。 因此,這個問題就變成最不規範的一個。 讓我們為她創造一個角色 /ansible/roles/ruby_rbenv/main.yml 讓我們開始填寫:

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

我們再次使用become_user指令在我們為此目的創建的用戶下工作。 由於 rbenv 安裝在其主目錄中,而不是全域安裝。 我們也使用 git 模組來克隆儲存庫,指定 repo 和 dest。

接下來,我們需要在 bashrc 中註冊 rbenv init 並將 rbenv 加入 PATH 中。 為此,我們有 lineinfile 模組:

- name: Add rbenv to PATH
  become_user: "{{ user }}"
  lineinfile:
    path: ~/.bashrc
    state: present
    line: 'export PATH="${HOME}/.rbenv/bin:${PATH}"'

- name: Add rbenv init to bashrc
  become_user: "{{ user }}"
  lineinfile:
    path: ~/.bashrc
    state: present
    line: 'eval "$(rbenv init -)"'

然後你需要安裝 ruby​​_build:

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

最後安裝紅寶石。 這是透過 rbenv 完成的,即簡單地使用 bash 命令:

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

我們說要執行哪個命令以及執行什麼命令。 然而,在這裡我們發現 ansible 在運行命令之前不會運行 bashrc 中包含的程式碼。 這意味著 rbenv 必須直接在同一腳本中定義。

下一個問題是由於從 ansible 的角度來看 shell 指令沒有狀態。 即不會自動檢查是否安裝了該版本的ruby。 我們可以自己做:

- 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

剩下的就是安裝捆綁器:

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

再次將我們的角色 ruby​​_rbenv 添加到主劇本中。

共享文件。

一般來說,到這裡就可以完成設定了。 接下來,剩下的就是運行 capistrano,它將複製程式碼本身,建立必要的目錄並啟動應用程式(如果一切配置正確)。 然而,capistrano經常需要額外的配置文件,例如 database.yml.env 它們可以像 nginx 的檔案和範本一樣被複製。 只有一處微妙之處。 在複製檔案之前,您需要為它們建立一個目錄結構,如下所示:

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

我們只指定一個目錄,如果需要,ansible 會自動建立父目錄。

Ansible Vault

我們已經認識到變數可以包含秘密數據,例如使用者的密碼。 如果您已經創建了 .env 申請文件,以及 database.yml 那麼一定還有更多這樣的關鍵數據。 最好將它們隱藏起來,以免被窺探。 為此目的,使用 安塞布爾保險庫.

讓我們為變數建立一個文件 /ansible/vars/all.yml (在這裡,您可以為不同的主機群組建立不同的文件,就像在庫存文件中一樣:生產.yml、staging.yml 等)。
所有必須加密的變數必須使用標準 yml 語法傳輸到此檔案:

# 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

之後可以使用以下命令加密該檔案:

ansible-vault encrypt ./vars/all.yml

當然,在加密的時候,你需要設定一個解密的密碼。 呼叫此命令後,您可以看到文件中的內容。

通過 ansible-vault decrypt 該檔案可以被解密、修改,然後再次加密。

您無需解密文件即可工作。 您將其加密存儲並使用參數運行劇本 --ask-vault-pass。 Ansible 將詢問密碼、檢索變數並執行任務。 所有資料將保持加密狀態。

多組主機和 ansibleVault 的完整指令如下所示:

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

但我不會給你劇本和角色的全文,你自己寫吧。 因為 ansible 就是這樣——如果你不明白需要做什麼,那麼它就不會為你做。

來源: www.habr.com

添加評論