在 2017 年夏季,我撰写了两篇关于使用 Ansible 的操作指南文章。在第一篇文章之后,我计划展示 copy
、systemd
、service
、apt
、yum
、virt
和 user
模块的示例。但我决定缩小第二部分的范围,专注于使用 yum
和 user
模块。我解释了如何设置基本的 Git SSH 服务器,并探讨了 command
、file
、authorized_keys
、yum
和 user
模块。在本文的第三部分中,我将深入探讨如何使用 Ansible 和 Prometheus 开源监控解决方案进行系统监控。
如果您按照前两篇文章的步骤操作,您应该已经:
- 安装了 Ansible 控制主机
- 在 Ansible 控制主机上创建了 SSH 密钥
- 将 SSH 密钥传播到您希望 Ansible 管理的所有机器
- 限制了所有机器上的 SSH 访问
- 安装了 Git SSH 服务器
- 创建了
git
用户,用于检入和检出 Git SSH 服务器的代码
从业务角度来看,您现在已经:
- 简化了主机管理
- 生成了一种可审计、可重复、自动化的方式来管理这些主机
- 开始为灾难恢复创建路径(通过 Ansible Playbook)
为了培养企业需要的技能,您必须能够看到一段时间内的资源利用率趋势。最终,这意味着设置某种监控工具。有很多工具可供选择,包括 Zabbix、Zenoss、Nagios、Prometheus 和许多其他工具。我使用过所有这些工具;您选择哪种解决方案很大程度上取决于:
- 预算
- 时间
- 熟悉程度
诸如 Nagios 之类的无 Agent 监控工具可以使用诸如 SNMP 之类的协议来监控主机并公开指标。这种方法可能有很多优点(例如不必安装 Agent)。但是,我发现 Prometheus 虽然基于 Agent,但非常容易设置,并且开箱即用提供了更多的指标,因此我将在本文中使用它。
设置 Prometheus
角色介绍
遗憾的是,Prometheus 没有可用的 Linux 软件包管理器存储库(Arch 用户存储库除外),或者至少在 Prometheus 下载页面上没有列出。有一个 Docker 镜像可用,在某些情况下可能是理想的选择,但如果目标机器上尚未安装 Docker,则需要运行额外的服务。对于本文,我将把预编译的二进制文件部署到每台主机。实际上只需要两个文件:二进制文件本身和一个 systemd
或 upstart
初始化文件。
因此,单个 Prometheus 安装 Playbook 可能会非常复杂;因此,现在是讨论过渡到 Ansible 角色 的好时机。简单来说,虽然您可以拥有一个巨大的 YAML 文件,但角色是一种组织较小任务集合的方式,这些任务可以包含在一个更大的Play中。通过示例更容易解释这一点。例如,假设您有一个需要在每台主机上的用户 ID,但您管理的某些服务器需要 Web 服务器,而其他服务器可能在其上安装了游戏服务器。您可能需要两个不同的 Playbook 来处理这种情况。考虑以下 Playbook:
示例 1:Create_user_with_ssh_key.yaml
- hosts: "{{ hostname }}"
gather_facts: false
tasks:
- name: create and/or change {{ username}}'s password
user:
name: "{{ username }}"
password: << some password hash>
- name: copy ssh keys
authorized_key:
key: "{{ item }}"
user: "{{ username }}"
state: present
exclusive: False
with_file:
- ../files/user1_ssh_key.pub
- ../files/user2_ssh_key.pub
在考虑这个问题时,有几个选项可用。
- 将此代码复制到每个将用于创建不同服务器的 Playbook 的开头
- 在运行服务器配置 Playbook 之前,手动运行此 Playbook
- 将
create_user_with_ssh_key.yaml
转换为任务,然后可以使用标准的 Ansible 实践将其包含在角色中
选项 1 在规模上是不可管理的。假设您必须更改密码或您正在创建的用户名。您将不得不找到包含此代码的所有 Playbook。
选项 2 是朝着正确方向迈出的一步。但是,每次创建服务器时,都需要额外的手动步骤。在家庭实验室中,这可能就足够了。但是,在一个多样化的环境中,可能有几个人遵循相同的流程来创建服务器,选项 2 依赖于管理员来记录并正确地遵循生成功能齐全的服务器以满足精确规范所需的所有步骤。
为了弥补这些缺点,选项 3 使用了 Ansible 的内置解决方案。它具有使用易于重现的服务器构建过程的优势。此外,在审核构建过程时(您是在使用我们之前设置的源代码控制,对吗?),审核员可以打开单个文件来确定 Ansible 自动使用了哪些任务文件来生成服务器构建。最终,这将是最佳的长期方法,并且尽早且经常地学习如何使用角色并养成使用角色的习惯是一个好主意。
使用正确的目录结构组织您的角色对于轻松的可审计性和您自身的成功至关重要。Ansible 文档对目录结构和布局提出了一些建议。我更喜欢类似于这样的目录布局:
└── production
├── playbooks
└── roles
├── common
│ ├── defaults
│ ├── files
│ ├── handlers
│ ├── tasks
│ └── vars
├── git_server
│ ├── files
│ ├── handlers
│ ├── tasks
│ └── vars
├── monitoring
│ ├── files
│ ├── handlers
│ ├── tasks
│ ├── templates
│ └── vars
Ansible 用于设计角色的系统起初可能有点令人困惑,尤其是因为有多个地方可以定义变量。在上面的情况下,在上面的情况下,我可以在 production
文件夹中创建一个 group_vars
目录,如下所示:
└── production/
└── group_vars/
└── all/
└── vars.yaml
将变量放在此文件中将使它们可用于放置在 production
文件夹中的任何角色。您可以在每个角色(例如 git_server
)下设置 vars
,从而使它们可用于给定角色的所有任务
└── environments/
└── production/
└── roles/
└── git_server/
└── vars/
└── main.yaml
最后,您可以在 Play 本身中指定变量。这些变量可以作用域限定为特定任务或 Play 本身(因此跨越同一 Play 中的多个任务)。
概括来说,您可以声明变量:
- 在角色级别,用于给定角色中的所有任务
- 在 Playbook 级别,用于 Play 中的所有任务
- 在单个任务内部
- 在 Ansible 主机文件(也称为清单)中;这主要用于机器变量,本文档未涵盖
决定创建变量的范围可能很困难,尤其是在权衡可维护性的易用性时。您可以将所有变量放在全局级别,这使得它们易于查找,但对于大型环境来说可能不是最好的主意。与此相反的是将所有变量放在单个任务内部,但如果您有很多变量,这可能会变成真正的麻烦。值得考虑您特定情况下的权衡取舍。
回到上面示例 1 中的小型 Playbook,我们可以像这样分解我们的文件:
├── production
│ ├── playbooks
│ └── roles
│ ├── common
│ │ ├── files
│ │ │ ├── user1_ssh_key.pub
│ │ │ └── user2_ssh_key.pub
│ │ ├── tasks
│ │ │ ├── create_user.yaml
│ │ │ ├── copy_ssh_key.yaml
tasks
文件的内容与单个整体式 Playbook 中的行相同
示例 2:create_user.yaml
- name: create and/or change {{ username}}'s password
user:
name: "{{ username }}"
password: << password hash >>
示例 3:copy_ssh_key.yaml
- name: copy ssh keys
authorized_key:
key: "{{ item }}"
user: "{{ username }}"
state: present
exclusive: False
with_file:
- user1_ssh_key.pub
- user2_ssh_key.pub
但是,已更改(可能)的是变量传递到 Ansible 的方式。您仍然可以使用 --extra-vars
选项。但是,为了演示另一种方法,我们将使用 vars/main.yaml
文件。vars/main.yaml
文件具有以下内容:
username: 'git'
password: 6$cmYTU26zdqOArk5I$WChA039bHQ8IXAo0W8GJxhk8bd9wvcY.DTUwN562raYjFhCkJSzSBm6u8RIgkaU8b3.Z3EmyxyvEZt8.OpCCN0
密码应该是哈希值,而不是明文密码。要在大多数 Linux 版本上生成哈希值,您可以运行以下 Python 命令:
python2.7 -c 'import crypt,getpass; print crypt.crypt(getpass.getpass(), "$1$awerwass")'
在上面的命令中,密码盐在 awerwass
中表示。这些只是我在键盘上随意敲击的随机字符。请勿在生产环境中使用相同的盐。
要使这些任务一起运行,您需要在 tasks
目录中创建一个 main.yaml
。它应具有以下内容:
---
- include: create_user.yaml
- include: copy_ssh_key.yaml
最后,创建一个具有以下内容的 Playbook:
- hosts: git
gather_facts: false
roles:
43- role: ../roles/common
您的目录结构应如下所示:
├── production
│ ├── playbooks
│ │ ├── common.yaml
│ └── roles
│ ├── common
│ │ ├── files
│ │ │ ├── user1_ssh_key.pub
│ │ │ └── user2_ssh_key.pub
│ │ ├── handlers
│ │ │ └── main.yaml
│ │ ├── tasks
│ │ │ ├── copy_ssh_key.yaml
│ │ │ ├── create_user.yaml
│ │ │ ├── main.yaml
│ │ └── vars
│ │ └── main.yaml
为 Prometheus 设置角色
既然我们已经介绍了创建角色的基础知识,那么让我们专注于创建 Prometheus 角色。如前所述,每个 Agent 运行只需要两个文件:一个服务(或 Upstart)文件和 Prometheus 二进制文件。以下是每个文件的示例:
示例 4:systemd prometheus-node-exporter.service 文件
[Unit]
Description=Prometheus Exporter for machine metrics.
After=network.target
[Service]
ExecStart=/usr/bin/prometheus_node_exporter
[Install]
WantedBy=multi-user.target
示例 5:Upstart 初始化文件
# Run prometheus_node_exporter
start on startup
script
/usr/bin/prometheus_node_exporter
end script
示例 6:systemd prometheus.service(服务器服务)文件
[Service]
User=prometheus
Group=prometheus
ExecStart=/usr/bin/prometheus -config.file=/etc/prometheus/prometheus.yaml -storage.local.path=/var/lib/prometheus/data -storage.local.retention=8928h -storage.local.series-file-shrink-ratio=0.3
ExecReload=/bin/kill -HUP $MAINPID
[Install]
WantedBy=multi-user.target
在我的环境中,使用 Ubuntu 机器(有和没有 systemd
)以及大量的 Red Hat 和 Arch 机器,我需要编写一个 Playbook,将正确的启动脚本分发到各个机器。您可以通过多种方式确定是部署 Upstart
还是 systemd
服务文件。Ansible 有一个内置的事实,称为 ansible_service_mgr
,可用于找出适当的服务管理器。
但是,我决定演示如何在 Gather Facts
阶段使用脚本为 Ansible 提供事实。这被称为 Ansible 本地事实。这些事实从 /etc/ansible/facts.d/
目录读取。此目录中的文件可以是 JSON、INI 或返回 JSON 的可执行文件。它们还需要具有文件扩展名 .fact
。我编写的简单 Bash 脚本检查 systemd
PID,如果找到,则返回一个 JSON,其值为 true
,如示例 7 所示:
示例 7:systemd_check.fact
#!/bin/bash
# Check for systemd if present return { 'systemd': 'true' }
systemd_pid=`pidof systemd`
if [ -z "$systemd_pid" ]; then
echo '{ "systemd": "false" }'
else
echo '{ "systemd": "true" }'
fi
考虑到这一点,我们可以开始构建一个简单的任务来帮助部署 Prometheus Agent。为了完成此操作,需要将本地 facts
文件复制到每台服务器,需要部署二进制文件和启动脚本,并且必须重新启动服务。以下是一个将部署 systemd_check.fact
脚本的任务。
示例 8:copy_local_facts.yaml
- name: Create the facts directory if does not exist
file:
path: /etc/ansible/facts.d
state: directory
- name: Copy the systemd facts file
copy:
src: systemd_check.fact
dest: /etc/ansible/facts.d/systemd_check.fact
mode: 0755
现在我们的自定义事实已部署,我们现在可以部署所需的二进制文件。但首先,让我们看一下将用于这些任务的变量文件。在此示例中,我选择使用本地化到各个角色的 vars/
目录。它目前看起来像这样:
示例 9:vars/main.yaml
exporter_binary: 'prometheus_node_exporter'
exporter_binary_dest: '/usr/bin/prometheus_node_exporter'
exporter_service: 'prometheus-node-exporter.service'
exporter_service_dest: '/etc/systemd/system/prometheus-node-exporter.service'
exporter_upstart: 'prometheus-node-exporter.conf'
exporter_upstart_dest: '/etc/init/prometheus-node-exporter.conf'
server_binary: 'prometheus'
server_binary_dest: '/usr/bin/prometheus'
server_service: 'prometheus.service'
server_service_dest: '/etc/systemd/system/prometheus.service'
prometheus_user: 'prometheus'
prometheus_server_name: 'prometheus'
client_information_dict:
'conan': '192.168.195.124:9100'
'confluence': '192.168.195.170:9100'
'smokeping': '192.168.195.120:9100'
'7-repo': '192.168.195.157:9100'
'server': '192.168.195.9:9100'
'ark': '192.168.195.2:9100'
'kids-tv': '192.168.195.213:9100'
'media-centre': '192.168.195.15:9100'
'nas': '192.168.195.195:9100'
'nextcloud': '192.168.199.117:9100'
'git': '192.168.195.126:9100'
'nuc': '192.168.195.90:9100'
'desktop': '192.168.195.18:9100'
现在,您可以忽略 client_information_dict
;稍后会用到它。
示例 10:tasks/setup_prometheus_node.yaml
---
- name: copy the binary to {{ exporter_binary_dest }}
copy:
src: "{{ exporter_binary }}"
dest: "{{ exporter_binary_dest }}"
mode: 0755
- name: put systemd service file in place
copy:
src: "{{ exporter_service }}"
dest: "{{ exporter_service_dest }}"
when:
- ansible_local.systemd_check.systemd == 'true'
- name: copy the upstart conf to {{ exporter_upstart_dest }}
copy:
src: "{{ exporter_upstart }}"
dest: "{{ exporter_upstart_dest }}"
when:
- ansible_local.systemd_check.systemd == 'false'
- name: update systemd and restart exporter systemd
systemd:
daemon-reload: true
enabled: true
state: restarted
name: "{{ exporter_service }}"
when:
- ansible_local.systemd_check.systemd == 'true'
- name: start exporter sysv service
service:
name: "{{ exporter_service }}"
enabled: true
state: restarted
when:
- ansible_local.systemd_check.systemd == 'false'
上面任务中最重要的一点是它引用了 ansible_local.systemd_check.systemd
。这可以分解为以下命名约定:<Ansible 如何生成事实> . <事实文件的名称> . <要检索的事实内部的键>
。Bash 脚本 systemd_check.fact
在 Gather Facts
阶段运行,然后存储在所有 Gather Facts
的 ansible_local
部分中。为了根据此事实做出决定,我检查它是 true
还是 false
。Ansible When 子句告诉 Ansible 仅在满足某些条件时才执行该特定任务。此任务的其余部分应该相当简单。它同时使用 systemd 和 service 模块,以确保将适当的服务管理器配置为启动 prometheus_node_exporter
。
设置服务器的任务非常相似:
示例 11:tasks/setup_Prometheus_server.yaml
---
- name: copy the server binary to {{ server_binary_dest }}
copy:
src: "{{ server_binary }}"
dest: "{{ server_binary_dest }}"
mode: 0755
when:
- inventory_hostname = 'prometheus'
- name: Ensure that /etc/prometheus exists
file:
state: directory
path: /etc/prometheus
owner: "{{ prometheus_user }}"
group: "{{ prometheus_user }}"
mode: 0755
when:
- inventory_hostname = 'prometheus'
- name: place prometheus config
template:
src: prometheus.yaml.j2
dest: /etc/prometheus/prometheus.yaml
when:
- inventory_hostname = 'prometheus'
- name: create /var/lib/promtheus/data
file:
state: directory
path: /var/lib/prometheus/data
recurse: true
owner: "{{ prometheus_user }}"
group: "{{ prometheus_user }}"
mode: 0755
when:
- inventory_hostname = 'prometheus'
- name: put systemd service file in place
copy:
src: "{{ server_service }}"
dest: "{{ server_service_dest }}"
when:
- ansible_local.systemd_check.systemd == 'true'
- inventory_hostname = 'prometheus'
- name: update systemd and restart prometheus server systemd
systemd:
daemon-reload: true
enabled: true
state: restarted
name: "{{ server_service }}"
when:
- ansible_local.systemd_check.systemd == 'true'
- inventory_hostname = 'prometheus'
notify: restart_prometheus_server
敏锐的观察者会注意到服务器任务中的几个新事物:
notify:
部分template:
模块
notify
部分是在满足某些条件时触发特定类型事件的一种方式。Ansible 处理程序最常用于触发服务重启(这正是上面发生的情况)。处理程序存储在角色内的 handlers
目录中。我的处理程序非常基本:
示例 12:handler/main.yaml
- name: restart_iptables
service:
name: iptables
state: restarted
enabled: true
- name: restart_prometheus_server
service:
name: "{{ server_service }}"
state: restarted
enabled: true
这只是允许我在 Prometheus 服务器上重新启动 prometheus.service
。
setup_prometheus_server.yaml
中的第二个关注点是 template:
部分。Ansible 中的模板提供了一些非常好的优势。Ansible 使用 Jinja2 作为其模板引擎;但是,Jinja 的完整解释超出了本教程的范围。本质上,您可以使用 Jinja2 模板创建一个配置文件,其中包含变量,这些变量的值在 Ansible Play 期间计算和替换。Prometheus 配置模板如下所示:
示例 13:templates/prometheus.yaml.j2
global:
scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute.
evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.
external_labels:
monitor: 'codelab-monitor'
scrape_configs:
# The job name is added as a label `job=<job_name>` to any timeseries scraped from this config.
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
- job_name: 'nodes'
static_configs:
{% for hostname, ip in client_information_dict.iteritems() %}
- targets: ['{{ ip }}']
labels: {'host': '{{ hostname }}' }
{% endfor %}
当处理模板部分时,在将文件放置在远程系统上之前,会自动删除 .j2
扩展名。模板中的小型 for 循环迭代我在变量文件中先前定义的 client_information_dict
。它只是创建了我希望 Prometheus 收集指标的虚拟机列表。
注意:如果您希望 Prometheus 显示主机名并且您的 DNS 设置正确,请改用此方法:
{% for hostname, ip in client_information_dict.iteritems() %}
- targets: ['{{ hostname }}:9100']
labels: {'host': '{{ hostname }}' }
{% endfor %}
还剩下一些收尾工作来完成 Prometheus 设置。我们需要创建 prometheus
用户,(可能)调整 iptables,将所有内容绑定到 main.yaml
中,并创建一个 Playbook 来运行。
Prometheus
用户的设置非常简单,如果您按照我之前的 Ansible 文章进行操作,您会非常熟悉:
示例 14:tasks/create_prometheus_user.yaml
---
- name: Ensure that the prometheus user exists
user:
name: "{{ prometheus_user }}"
shell: /bin/false
这里唯一的重大区别是我将 Shell 设置为 /bin/false
,以便用户可以运行服务但不能登录。
如果您正在运行 iptables,则需要确保打开端口 9100,以便 Prometheus 可以从其客户端收集指标。以下是一个执行此操作的简单任务:
示例 15:tasks/iptables.yaml
---
- name: Open port 9100
lineinfile:
dest: /etc/sysconfig/iptables
insertbefore: "-A INPUT -j OS_FIREWALL_ALLOW"
line: "-A INPUT -p tcp -m state --dport 9100 --state NEW -j ACCEPT"
notify: restart_iptables
when:
- ansible_os_family == "RedHat"
注意:我仅在我的 Red Hat 系列虚拟机上运行 iptables。如果您在所有虚拟机上运行 iptables,请删除 when:
部分。
main.yaml
如下所示:
示例 16:tasks/main.yaml
---
- include: create_prometheus_user.yaml
- include: setup_prometheus_node.yaml
- include: setup_prometheus_server.yaml
- include: prometheus_iptables.yaml
最后一步是创建一个 Playbook,其中包含完成任务所需的角色:
示例 17:playbooks/monitoring.yaml
- hosts: all
roles:
- role: ../roles/common
- role: ../roles/monitoring
将所有内容绑定在一起
我知道看起来有很多文本需要阅读,但是使用 Ansible 的概念非常简单。通常只是知道如何完成您着手执行的任务,然后找到合适的 Ansible 模块来帮助完成它们。如果您一直按照本演练进行操作,则您的布局应类似于这样:
├── playbooks
│ ├── git_server.yaml
│ ├── monitoring.yaml
└── roles
├── common
│ ├── files
│ │ ├── systemd_check.fact
│ │ ├── user1_ssh_key.pub
│ │ └── user2_ssh_key.pub
│ ├── handlers
│ │ └── main.yaml
│ ├── tasks
│ │ ├── copy_systemd_facts.yaml
│ │ ├── main.yaml
│ │ ├── push_ssh_key.yaml
│ │ ├── root_ssh_key_only.yaml
│ └── vars
│ └── main.yaml
├── monitoring
│ ├── files
│ │ ├── prometheus
│ │ ├── prometheus_node_exporter
│ │ ├── prometheus-node-exporter.conf
│ │ ├── prometheus-node-exporter.service
│ │ ├── prometheus.service
│ │ └── systemd_check.fact
│ ├── handlers
│ │ └── main.yaml
│ ├── tasks
│ │ ├── create_prometheus_user.yaml
│ │ ├── main.yaml
│ │ ├── prometheus_iptables.yaml
│ │ ├── setup_prometheus_node.yaml
│ │ └── setup_prometheus_server.yaml
│ ├── templates
│ │ └── prometheus.yaml.j2
└── vars
└── main.yaml
要运行您的 Playbook,请输入:
[root@ansible-host production]# ansible-playbook -i <path to host file> playbooks/monitoring.yaml
您现在应该能够创建用户、推送 SSH 密钥、检查 systemd
的存在,并将 prometheus_node_exporter
或 Prometheus 服务器二进制文件部署到相应的服务器。Prometheus 应该使用基本配置进行初始化,包括监控角色中 vars/main.
yaml
文件中指定的主机列表。
恭喜!您现在已经自动化了基本 Prometheus 服务器的安装和配置,并配置了您的主机开始传输数据。作为一个令人愉快的副作用,您还有效地记录了完成目标所需的所有步骤。
附录:当我构思这个系列时,我打算研究在 OpenShift 中安装 Prometheus;但是,在查看 OpenShift 的 Ansible 安装程序的文档时,我发现它已经包含在 Playbook 中,并且是安装程序中的一个选项。
1 条评论