不久前,我需要寫幾台 Ansible playbook 來準備部署 Rails 應用程式的伺服器。 而且,令人驚訝的是,我沒有找到簡單的逐步手冊。 我不想在不了解發生了什麼的情況下複製別人的劇本,最後我不得不閱讀文檔,自己收集所有內容。 也許我可以在本文的幫助下幫助某人加快這個過程。
首先要了解的是,ansible 為您提供了一個方便的介面,可以透過 SSH 在遠端伺服器上執行預先定義的操作清單。 這裡沒有魔法,您無法安裝插件並使用 docker、監控和其他開箱即用的好東西來零停機部署您的應用程式。 為了寫一本劇本,你必須知道你到底想做什麼、如何做。 這就是為什麼我對 GitHub 上現成的劇本或諸如“複製並運行,它會起作用”之類的文章不滿意。
我們需要什麼?
正如我已經說過的,為了寫一本劇本,你需要知道你想做什麼以及如何做。 讓我們決定我們需要什麼。 對於 Rails 應用程序,我們需要幾個系統套件:nginx、postgresql(redis 等)。 此外,我們還需要特定版本的 ruby。 最好透過 rbenv(rvm、asdf...)安裝。 以 root 使用者身分執行所有這些總是一個壞主意,因此您需要建立一個單獨的使用者並配置他的權限。 之後,您需要將我們的程式碼上傳到伺服器,複製 nginx、postgres 等的配置並啟動所有這些服務。
結果,動作順序如下:
- 以 root 身分登入
- 安裝系統套件
- 建立新用戶,配置權限,ssh金鑰
- 配置系統包(nginx等)並運行它們
- 我們在資料庫中建立使用者(可以立即建立資料庫)
- 以新使用者登入
- 安裝 rbenv 和 ruby
- 安裝捆綁器
- 上傳應用程式程式碼
- 啟動 Puma 伺服器
此外,最後階段可以使用 capistrano 完成,至少開箱即用,它可以將程式碼複製到發布目錄中,在成功部署後使用符號連結切換版本,從共享目錄複製配置,重新啟動 puma 等。 所有這些都可以使用 Ansible 來完成,但為什麼呢?
文件結構
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-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