不久前,我需要编写几个 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金库
我们已经认识到变量可以包含秘密数据,例如用户的密码。 如果您已经创建了 .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 就是这样——如果你不明白需要做什么,那么它就不会为你做。
来源: habr.com