不久前,我需要编写几本有趣的剧本,以准备用于部署Rails应用程序的服务器。 而且,令人惊讶的是,我没有找到简单的分步指南。 我不想在不了解正在发生的事情的情况下复制别人的剧本,因此不得不阅读文档,自己收集所有内容。 也许有人可以通过本文帮助您加快这一过程。
首先要了解的是,ansible为您提供了一个方便的界面,用于通过SSH在远程服务器上执行预定义的操作列表。 这里没有万能的法宝,您无法安装插件,也无法摆脱零宕机时间,使用docker,监控和其他工具部署应用程序。 为了编写剧本,您必须确切地知道自己想做什么以及如何做。 因此,我不喜欢github上现成的剧本,也不喜欢类似的文章:“复制并运行,它将起作用。”
我们需要什么?
正如我所说,要编写一本剧本,您需要知道您想做什么以及如何做。 让我们来决定我们需要什么。 对于Rails应用程序,我们将需要几个系统软件包:nginx,postgresql(redis等)。 除此之外,我们还需要特定版本的红宝石。 最好通过rbenv(rvm,asdf ...)安装它。 从用户的根目录运行所有这些总是一个坏主意,因此您需要创建一个单独的用户并为其配置权限。 之后,您需要将我们的代码上传到服务器,复制nginx,postgres等的配置并运行所有这些服务。
结果,动作顺序如下:
- 以root身份登录
- 安装系统软件包
- 创建一个新用户,配置权限,ssh密钥
- 配置系统软件包(nginx等)并运行它们
- 在数据库中创建用户(您可以立即创建数据库)
- 以新用户身份登录
- 安装rbenv和ruby
- 安装捆绑器
- 填写申请代码
- 我们启动Puma服务器
此外,最后的步骤可以使用capistrano完成,至少她可以将代码从框中复制到发行目录中,在成功部署后使用符号链接切换发行版,从共享目录复制配置,重新启动puma等。 所有这些都可以通过Ansible完成,但是为什么呢?
档案结构
Ansible的所有文件都有严格的文件结构 ,因此最好将所有这些文件保存在单独的目录中。 而且它是否将在rails应用程序本身中还是单独出现并不重要。 您可以将文件存储在单独的git存储库中。 就我个人而言,最方便的是在应用程序的rails目录的/ config中创建ansible目录并将所有内容存储在一个存储库中。
简单的剧本
Playbook是yml文件,描述了使用特殊语法ansible应该做什么以及如何做。 让我们创建第一个什么都不做的剧本:
--- - name: Simple playbook hosts: all
在这里,我们只是说我们的剧本称为Simple Playbook
剧本,其内容应在所有主机上运行。 我们可以将其保存在名称为playbook.yml
的/ ansible目录中,然后尝试运行:
ansible-playbook ./playbook.yml PLAY [Simple Playbook] ************************************************************************************************************************************ skipping: no hosts matched
Ansible说,它不知道与所有列表匹配的主机。 它们必须在特殊的清单文件中列出。
让我们在同一个ansible目录中创建它:
123.123.123.123
因此,只需指定主机(理想情况下,是用于测试的VPS主机,或者您可以注册localhost),然后将其保存为inventory
。
您可以尝试使用invetory文件运行ansible:
ansible-playbook ./playbook.yml -i inventory PLAY [Simple Playbook] ************************************************************************************************************************************ TASK [Gathering Facts] ************************************************************************************************************************************ PLAY RECAP ************************************************************************************************************************************
如果您具有对指定主机的ssh访问权限,则ansible将连接并收集有关远程系统的信息。 (默认任务[收集事实]),然后将提供简短的进度报告(PLAY RECAP)。
默认情况下,将使用您用来登录系统的用户名进行连接。 它很可能不在主机上。 在剧本文件中,您可以使用remote_user指令指定用于连接的用户。 此外,关于远程系统的信息通常对于您来说是不必要的,并且您不应浪费时间来收集它。 您也可以关闭此任务:
--- - name: Simple playbook hosts: all remote_user: root become: true gather_facts: no
尝试再次运行该剧本,并确保连接正常。 (如果指定了用户的根,则还必须指定turn:true指令以获取提升的权限。如文档所述: become set to 'true'/'yes' to activate privilege escalation.
尽管不清楚原因) 。
可能是由于python解释器无法确定的ansible导致错误,然后可以手动指定它:
ansible_python_interpreter: /usr/bin/python3
可以在whereis python
命令中找到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本身不会这样做。
尝试再次运行该剧本,并确认已安装软件包。
创建新用户。
为了与用户合作,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),它们适用于所有主机(全部)。
"{{ user_password | password_hash('sha512') }}"
也很有趣。 事实是, user_add
像您手动设置的那样通过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是将角色连接到剧本时将加载并执行的主文件,可以在其中连接其他角色文件)。 现在,您可以将与用户相关的所有任务传输到该文件:
# 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
另外,在所有其他任务之前执行系统更新可能很有意义,为此,您可以重命名在pre_tasks
中定义了它们的tasks
块。
Nginx设置
Nginx应该已经与我们一起安装,您需要对其进行配置和运行。 让我们马上就扮演角色吧。 创建一个文件结构:
- ansible - roles - nginx - files - tasks - main.yml - templates
现在我们需要文件和模板。 两者之间的区别在于,ansible文件是按原样直接复制的。 并且模板应具有扩展名j2,并且它们可以使用相同的双大括号来使用变量的值。
让我们在main.yml
文件中包含nginx。 为此,我们有一个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配置文件(您可以直接从服务器获取它,也可以自己编写)。 以及位于sites_available目录中的应用程序的配置文件(这不是必需的,但很有用)。 在第一种情况下,我们使用复制模块来复制文件(该文件必须在/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将在复制前替换为模板。 如果您将剧本用于不同的主机组,这将很有用。 例如,我们可以补充库存文件:
[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。 复制配置文件后,我们需要从sites_available在my_app.conf上的sitest_enabled中创建符号链接。 并重新启动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角色添加到主要剧本中。
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的角色。
通过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
我们再次使用begin_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
最后安装ruby。 这是通过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。
下一个问题是shell命令在ansible中没有状态。 也就是说,不会自动检查是否已安装此版本的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保险库
我们已经偶然发现这样一个事实,即诸如用户密码之类的秘密数据可能会出现在变量中。 如果为应用程序和database.yml
.env
创建了一个.env
文件,则应该有更多此类关键数据。 遮住他们的眼睛,这将是很好的选择。 Ansible保险库用于此目的。
让我们为变量/ansible/vars/all.yml
创建一个文件(在这里您可以为不同的主机组创建不同的文件,就像在清单文件中一样:production.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将要求输入密码,获取变量并完成任务。 所有数据将保持加密状态。
几个主机组和ansible Vault的完整命令如下所示:
ansible-playbook -i inventory ./playbook.yml -l "staging" --ask-vault-pass
而且我不会给您剧本和角色的全文,而是为您自己写。 因为这很烦人-如果您不了解需要做什么,那么他就不会做。