Ansible角色

12304 字
62 分钟
Ansible角色

Ansible角色#

[TOC]


彩蛋#

Ansible 有一个非常有趣的小彩蛋:当它检测到你的系统中安装了 cowsay 时,就会自动调用它,把原本枯燥的任务名称(比如 PLAY RECAPTASK [...])放进小牛的对话气泡里,让输出界面变得生动有趣

Terminal window
____________
< PLAY RECAP >
------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
Client: ok=5 changed=3 failed=0 skipped=0 ignored=0
Server: ok=8 changed=6 failed=0 skipped=0 ignored=0

除了默认的小牛,cowsay 还有很多其他 ASCII 艺术图案

  • 查看可用图案:你可以运行 cowsay -l 来查看所有可用的图案
Terminal window
[root@Zabbix ~]# cowsay -l
Cow files in /usr/share/cowsay:
cheese cower default dragon dragon-and-cow elephant elephant-in-snake eyes milk.....

指定图案:你可以通过修改 Ansible 的配置文件 /etc/ansible/ansible.cfg 来指定默认使用的图案

Terminal window
# 在 [defaults] 部分添加或修改
[defaults]
# 设置为 'dragon',下次运行就会看到一条龙在说话
cow_selection = dragon

🚫 觉得太花哨?关掉它!

  • 个人觉得比默认的 * 好玩多了

编辑 Ansible 的主配置文件 /etc/ansible/ansible.cfg

Terminal window
# 在 [defaults] 部分
[defaults]
nocows = 1

概述#

  • ==roles== 是对playbook的一种重新编排
Terminal window
🛠️ 如何创建角色?
'ansible-galaxy init 角色名'
# 它能一键生成标准的目录结构
[root@m01 ansible]# mkdir roles
[root@m01 ansible]# cd roles/
[root@m01 roles]# ansible-galaxy init backup
- Role backup was created successfully
# roles/backup--->专门用来备份
'当然我们还可以在roles目录下创建别的'
[root@m01 roles]# tree ./backup/
./backup/
├── defaults ❌️
│   └── main.yml
├── files
├── handlers
│   └── main.yml
├── meta ❌️
│   └── main.yml
├── README.md ❌️
├── tasks
│   └── main.yml
├── templates
├── tests ❌️
│   ├── inventory
│   └── test.yml
└── vars
└── main.yml
# 我们先把没有用的删除掉!
[root@m01 roles]# cd backup/
[root@m01 backup]# rm -rf defaults/ meta/ tests/ README.md
[root@m01 backup]# tree
.
├── files # 静态文件
├── handlers # 触发器
│   └── main.yml✅️自动创建
├── tasks # 核心任务
│   └── main.yml✅️自动创建
├── templates # 模版文件(.j2)
└── vars # 固定变量
└── main.yml✅️自动创建
# 我们经常用的就这几个!
✅️我们直接在main.yml里面编写即可!

backup重构#

Terminal window
1)拷贝配置文件
# 到template目录下
[root@m01 backup]# cp ~/rsyncd.conf ./templates/rsyncd.conf.j2
# rsync的配置文件
[root@m01 backup]# cd templates/
[root@m01 templates]# echo rsync_backup:123 > rsync.passwd.j2
[root@m01 templates]# ls
rsyncd.conf.j2 rsync.passwd.j2
# 配置文件和密码文件
[root@m01 templates]# tree ../templates/
../templates/
├── rsyncd.conf.j2
└── rsync.passwd.j2
2)编辑核心剧本
[root@m01 templates]# cd ..
[root@m01 backup]# cd ..
# 要在roles目录下创建playbook
[root@m01 roles]# vim site.yml
# 从hosts开始写
- hosts: backup
# 后面要改为all,会有不同的角色!
roles:
- role: backup
when: ansible_hostname is match "backup"
# when判断和role对齐
# 只有主机为backup才执行
3)恢复快照&&免密登录
[root@m01 roles]# ssh-copy-id -i ~/.ssh/my_key.pub 172.16.1.41
[root@m01 roles]# ssh backup "hostname -I"
10.0.0.41 172.16.1.41
'密码成功'
4)检测与测试
[root@m01 roles]# ansible-playbook --syntax-check site.yml
playbook: site.yml
[root@m01 roles]# ansible-playbook site.yml
PLAY [backup]
**********************
TASK [Gathering Facts]
ok: [backup]
'收集信息'
**********************
TASK [backup : Install rsync server]
ok: [backup]
'安装rsync'
**********************
TASK [backup : Configure rsync file]
changed: [backup] => (item={'src': 'rsyncd.conf.j2', 'dest': '/etc/rsyncd.conf', 'mode': '0644'})
changed: [backup] => (item={'src': 'rsync.passwd.j2', 'dest': '/etc/rsync.passwd', 'mode': '0600'})
# 拷贝文件--->配置文&&密码文件
**********************
TASK [backup : Create group "www"]
changed: [backup]
"创建组"
**********************
TASK [backup : Create user "www"]
changed: [backup]
'创建用户'
**********************
TASK [backup : Create dir]
changed: [backup] => (item=/backup)
changed: [backup] => (item=/data)
'创建目录'
**********************
TASK [backup : Start rsync server]
changed: [backup]
'启动&&开机自启'
'测试验证'
[root@m01 roles]# rsync -avz /etc/passwd rsync_backup@172.16.1.41::backup
Password: ---'密码123'
sent 841 bytes received 43 bytes 353.60 bytes/sec
[root@backup ~]#ls /backup/passwd
/backup/passwd
  • roles/backup/vars/main.yml
    • 存放着固定变量
user: www
uid: 888
rc_port: 873
dir_1: /backup
dir_2: /data
  • roles/backup/template/rsyncd.conf.j2
    • 模版配置文件
uid = {{user}}
gid = {{user}}
port = {{rc_port}}
fake super = yes
use chroot = no
max connections = 200
timeout = 600
ignore errors
read only = false
list = false
auth users = rsync_backup
secrets file = /etc/rsync.passwd
pid file = /var/run/rsyncd.pid
lock file = /var/run/rsync.lock
log file =/var/log/rsyncd.log
############################
[backup]
path = {{dir_1}}
[data]
path = {{dir_2}}

  • roles/backup/tasks/main.yml
    • 任务核心
- name: Install rsync server
yum:
name: rsync
state: present
- name: Configure rsync file
template:
src: "{{item.src}}"
dest: "{{item.dest}}"
mode: "{{item.mode}}"
notify: Restart rsync
# 只要修改了,就触发handler重启
loop:
# src能够自动识别template里面的模版文件
- src: rsyncd.conf.j2
dest: /etc/rsyncd.conf
mode: "0644"
- src: rsync.passwd.j2
dest: /etc/rsync.passwd
mode: "0600"
- name: Create group "{{user}}"
group:
name: "{{user}}"
gid: "{{uid}}"
- name: Create user "{{user}}"
user:
name: "{{user}}"
uid: "{{uid}}"
group: "{{user}}"
shell: /sbin/nologin
create_home: false
- name: Create dir
file:
path: "{{item}}"
state: directory
owner: "{{user}}"
group: "{{user}}"
loop:
# 因为变量没有在路径中所以要加引号
# /tmp/{{dir}}-->这种不需要加引号
- "{{dir_1}}"
- "{{dir_2}}"
- name: Start rsync server
systemd:
name: rsyncd
state: started
enabled: yes
  • roles/backup/handlers/main.yml
    • 触发器,修改配置文件--->给handler发信号
- name: Restart rsync
# 和notify的名字相同
systemd:
name: rsyncd
state: restarted

nfs重构#

Terminal window
1)恢复快照&&免密
[root@m01 roles]# ssh-copy-id -i ~/.ssh/my_key.pub 172.16.1.31
[root@m01 roles]# ssh nfs01 "hostname -I"
10.0.0.31 172.16.1.31
2)创建角色
[root@m01 roles]# ansible-galaxy init nfs
- Role nfs was created successfully
[root@m01 roles]# cd nfs/
[root@m01 nfs]# ls
defaults files handlers meta README.md tasks templates tests vars
[root@m01 nfs]# rm -rf defaults/ meta/ README.md tests/
[root@m01 nfs]# ls
files handlers tasks templates vars
3)拷贝配置文件
[root@m01 nfs]# cp /home/ansible/exports ./templates/exports.j2
[root@m01 nfs]# cat ./templates/exports.j2
{{dir}} 172.16.1.0/24(rw,sync,all_squash,anonuid={{ id }},anongid={{ id }})
4)编辑核心剧本
[root@m01 nfs]# cd ..
# 继续上一个的写!
[root@m01 roles]# vim site.yml
- hosts: all
# 注意看自己的主机清单!
roles:
- role: backup
when: ansible_hostname is match "backup"
# when判断和role对齐
# 只有主机为backup才执行
- role: nfs
when: ansible_hostname is match "nfs"
[root@m01 roles]# tree nfs/
nfs/
├── files
├── handlers
│   └── main.yml
├── tasks
│   └── main.yml
├── templates
│   └── exports.j2
└── vars
└── main.yml
5)测试&验证
[root@m01 roles]# ansible-playbook --syntax-check site.yml
playbook: site.yml
[root@m01 roles]# showmount -e 172.16.1.31
clnt_create: RPC: ❌️Unable to receive
[root@m01 roles]# ansible-playbook site.yml
TASK [backup : Install rsync server]
skipping: [db02]
skipping: [nfs01]
skipping: [web01]
skipping: [web02]
ok: [backup]
'只有backup执行,其他都跳过'
**********************
.................
'先跑第一个角色backup,再跑nfs'
**********************
TASK [nfs : Install nfs server]
skipping: [db02]
skipping: [backup]
skipping: [web02]
skipping: [web01]
ok: [nfs01]
'只有nfs执行!!'
**********************
nfs01 : ok=7 changed=6
[root@m01 roles]# showmount -e 172.16.1.31
Export list for 172.16.1.31:
/data/wp 172.16.1.0/24✅️
'没问题👌'
user: www
dir: /data/wp
id : 888
- name: Install nfs server
yum:
name: nfs-utils
state: present
- name: Configure nfs file
template:
src: exports.j2
dest: /etc/exports
notify: Restart nfs
- name: Create group "{{user}}"
group:
name: "{{user}}"
gid: "{{id}}"
- name: Create user "{{user}}"
user:
name: "{{user}}"
uid: "{{id}}"
group: "{{user}}"
shell: /sbin/nologin
create_home: false
- name: Create dir "{{dir}}"
# 递归创建了目录/data/wp
# 这两个目录的属主都是www
file:
path: "{{dir}}"
state: directory
owner: "{{user}}"
group: "{{user}}"
- name: Start NFS Server
systemd:
name: nfs
state: started
enabled: yes
- name: Restart nfs
systemd:
name: nfs
state: restarted

主配置文件#

Terminal window
[root@Zabbix ~]# tree /ansible/
/ansible/
└── roles
├── Client
│ ......
├── Server
......
└── site.yml
'hosts可以写在 - name '
- name: 配置 Server 主机
hosts: Server
roles:
- Server
- name: 配置 Client 主机
hosts: Client
roles:
- Client
- name: Server 测试验证
hosts: Server
tasks:
- name: Result test
shell: 'unbound-control dump_cache | grep jd.com'
register: result_end
- name: Print result_end
debug:
msg: "{{result_end.stdout_lines}}"

面试题#

Terminal window
请问用过ansible吗?
你都用来干啥?
用过、--->配置变更、数据收集facts模块(资产管理)、批量安装部署服务
那你用过哪些模块?
file copy template yum systemd cron handles vars when loop
我执行一条命令、但是看不到结果怎么办?
使用变量注册 register

项目实训#

短主机名FQDN管理 IP (ens1)NAT IP (ens0)业务 IP (ens2)存储 IP (ens3)
ops-adminops-admin.example.com10.2.1.1010.0.0.10--
gitlab-servergitlab-server.example.com10.2.1.2010.0.0.20--
zabbix-serverzabbix-server.example.com10.2.1.3010.0.0.30--
jumpserverjumpserver.example.com10.2.1.4010.0.0.40--
waf01waf01.example.com10.2.1.5010.0.0.5010.2.2.50-
lb01lb01.example.com10.2.1.6010.0.0.6010.2.2.60-
web01web01.example.com10.2.1.7110.0.0.7110.2.2.7110.2.3.71
web02web02.example.com10.2.1.7210.0.0.7210.2.2.7210.2.3.72
share01share01.example.com10.2.1.8010.0.0.80-10.2.3.80
clientclient.example.com-10.0.0.9010.2.2.90-
网卡Connection ID
ens0ens0-nat
ens1ens1-mg
ens2ens2-bi
ens3ens3-st

Ansible管理#

环境准备#

Terminal window
[root@ops-admin ~]# yum -y install tar zip
[root@ops-admin ~]# curl -LsSf https://astral.sh/uv/install.sh | bash
downloading uv 0.11.25 x86_64-unknown-linux-gnu
installing to /root/.local/bin
uv
uvx
everything's installed!
[root@ops-admin ~]# echo 'eval "$(uv generate-shell-completion bash)"' >> ~/.bashrc
[root@ops-admin ~]# echo 'eval "$(uvx --generate-shell-completion bash)"' >> ~/.bashrc
[root@ops-admin ~]# echo 'export UV_INDEX_URL="https://mirrors.aliyun.com/pypi/simple/"' >> ~/.bashrc
[root@ops-admin ~]# source ~/.bashrc
[root@ops-admin ~]# mkdir /Projects
[root@ops-admin ~]# cd /Projects/
[root@ops-admin Projects]# python3 --version
Python 3.12.13
[root@ops-admin Projects]# uv venv .venv --python 3.12
Using CPython 3.12.13 interpreter at: /usr/bin/python3.12
Creating virtual environment at: .venv
Activate with: source .venv/bin/activate
[root@ops-admin Projects]# source .venv/bin/activate
(.venv) [root@ops-admin Projects]# uv pip install ansible
Resolved 10 packages in 3.51s
Prepared 10 packages in 8.88s
Installed 10 packages in 3.89s
+ ansible==14.1.0
+ ansible-core==2.21.1
+ cffi==2.0.0
+ cryptography==49.0.0
+ jinja2==3.1.6
+ markupsafe==3.0.3
+ packaging==26.2
+ pycparser==3.0
+ pyyaml==6.0.3
+ resolvelib==1.2.1
(.venv) [root@ops-admin Projects]# ansible --version
ansible [core 2.21.1]
(.venv) [root@ops-admin Projects]# cat > ./ansible.cfg <<EOF
[defaults]
host_key_checking = False
deprecation_warnings = False
interpreter_python = auto_silent
inventory = ./inventory
gathering = smart
# 开启智能模式:如果有缓存就用缓存,没有才去收集
fact_caching = jsonfile
# 指定缓存方式为 jsonfile(存成本地文件)
fact_caching_connection = /tmp/ansible_fact_cache
# 指定缓存文件存到哪里
fact_caching_timeout = 86400
# 缓存多久失效(秒),这里设置 24 小时
[privilege_escalation]
become = true
become_method = sudo
become_user = root
become_ask_pass = false
EOF
(.venv) [root@ops-admin Projects]# ssh-keygen -t rsa -b 4096 -f ~/.ssh/my_rsa -N ''
Generating public/private rsa key pair.
Your identification has been saved in /root/.ssh/my_rsa
Your public key has been saved in /root/.ssh/my_rsa.pub
The key fingerprint is:
SHA256:ySF1tKhH31U5H9ZlMXlHrHSB3dgRuAJs1d32l0hnCn4 root@ops-admin
The key's randomart image is:
+---[RSA 4096]----+
| o.+.. =@^|
| . * o =o&%|
| . = + o.O+O|
| = + + E..+|
| . S . + .|
| . |
| |
| |
| |
+----[SHA256]-----+
(.venv) [root@ops-admin Projects]# sed -i '/id_rsa/a\IdentityFile ~/.ssh/my_rsa' /etc/ssh/ssh_config
(.venv) [root@ops-admin Projects]# grep -A1 id_rsa /etc/ssh/ssh_config
# IdentityFile ~/.ssh/id_rsa
IdentityFile ~/.ssh/my_rsa
(.venv) [root@ops-admin Projects]# systemctl restart sshd
(.venv) [root@ops-admin Projects]# cat > /etc/hosts <<EOF
127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4
::1 localhost localhost.localdomain localhost6 localhost6.localdomain6
10.2.1.10 ops-admin ops-admin.example.com
10.2.1.20 gitlab-server gitlab-server.example.com
10.2.1.30 zabbix-server zabbix-server.example.com
10.2.1.40 jumpserver jumpserver.example.com
10.2.1.50 waf01 waf01.example.com
10.2.1.60 lb01 lb01.example.com
10.2.1.71 web01 web01.example.com
10.2.1.72 web02 web02.example.com
10.2.1.80 share01 share01.example.com
EOF
(.venv) [root@ops-admin Projects]# awk '/^10\.2\.1/ {print $2}' /etc/hosts | grep -v ops-admin > ./inventory
# 排除自身 ops-admin
(.venv) [root@ops-admin Projects]# cat ./inventory
gitlab-server
zabbix-server
jumpserver
waf01
lb01
web01
web02
share01
[root@ops-admin ~]# ll ~/.ssh/my_rsa.pub
# 如何批量分发密钥呢?-i指定公钥
(.venv) [root@ops-admin Projects]# vim /server/sh/ssh-key.sh

密钥分发#

#!/bin/bash
# ============================================
# 批量分发 SSH 公钥脚本
# 从 /etc/hosts 读取 10.2.1.x 网段主机,
# 通过管理网将公钥分发到所有节点
# ============================================
# ---- 配置区 ----
PUBKEY="$HOME/.ssh/my_rsa.pub" # 要分发的公钥路径
PASSWORD="1" # 目标主机的 root 密码
# -----------------
echo "========================================"
echo " 批量分发 SSH 公钥"
echo " 公钥: $PUBKEY"
echo "========================================"
echo ""
# 检查公钥是否存在
if [ ! -f "$PUBKEY" ]; then
echo "错误: 公钥文件 $PUBKEY 不存在,请先生成!"
exit 1
fi
# 检查 sshpass 是否安装
if ! command -v sshpass &>/dev/null; then
echo "sshpass 未安装,正在安装..."
dnf install -y sshpass &>/dev/null || yum install -y sshpass &>/dev/null
fi
# 从 /etc/hosts 提取所有 10.2.1.x 的短主机名
# awk: 匹配 10.2.1 开头的行,打印第二列(短名)
HOSTS=$(awk '/^10\.2\.1/ {print $2}' /etc/hosts)
if [ -z "$HOSTS" ]; then
echo "错误: /etc/hosts 中没有找到 10.2.1.x 的主机!"
exit 1
fi
echo "目标主机列表:"
echo "$HOSTS" | tr '\n' ' '
echo ""
echo ""
# 逐个分发
for host in $HOSTS; do
echo ">>> 分发到 $host ..."
# sshpass: 自动输入密码
# ssh-copy-id: 将公钥复制到目标主机
# -i: 指定公钥文件
# -o StrictHostKeyChecking=no: 跳过首次连接 yes/no 确认
# -o UserKnownHostsFile=/dev/null: 不记录主机指纹(可选,干净)
sshpass -p "$PASSWORD" ssh-copy-id \
-i "$PUBKEY" \
-o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null \
root@$host 2>/dev/null
if [ $? -eq 0 ]; then
echo " ✓ $host 分发成功"
else
echo " ✗ $host 分发失败,请检查网络和密码"
fi
echo ""
done
echo "========================================"
echo " 分发完成!验证免密登录:"
echo " ssh root@ops-admin"
echo "========================================"
Terminal window
(.venv) [root@ops-admin Projects]# ansible --version | grep 'config file'
config file = /Projects/ansible.cfg
(.venv) [root@ops-admin Projects]# grep inventory ./ansible.cfg
inventory = ./inventory
(.venv) [root@ops-admin Projects]# ansible all -m ping
jumpserver | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3.12"
},
"changed": false,
"ping": "pong"
}
waf01 | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3.12"
},
"changed": false,
"ping": "pong"
}
gitlab-server | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3.12"
},
"changed": false,
"ping": "pong"
}
zabbix-server | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3.12"
},
"changed": false,
"ping": "pong"
}
lb01 | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3.12"
},
"changed": false,
"ping": "pong"
}
share01 | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3.12"
},
"changed": false,
"ping": "pong"
}
web01 | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3.12"
},
"changed": false,
"ping": "pong"
}
web02 | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3.12"
},
"changed": false,
"ping": "pong"
}

📌 当前环境

组件路径/状态
Ansible 项目/Projects/
虚拟环境需手动 source venv/bin/activate
配置文件/Projects/ansible.cfg(已配置提权)
Inventory/Projects/inventory(8 台受控节点)
控制节点ops-admin (10.2.1.10)
Terminal window
(.venv) [root@ops-admin Projects]# vim playbook-init.yml
---
# ============================================
# Ansible 初始化剧本 — 第二组
# 用法: cd /Projects && source venv/bin/activate && ansible-playbook playbook-init.yml
# ============================================
- hosts: all
gather_facts: yes
tasks:
# ---- 主机名解析 ----
- name: "拷贝 /etc/hosts 到所有节点"
copy:
src: /etc/hosts
dest: /etc/hosts
owner: root
group: root
mode: '0644'
# ---- 软件包 ----
- name: "安装必要软件包"
package:
name:
- tar
- zip
- unzip
state: present
# ---- 创建 dev 用户和组 ----
- name: "创建开发组 dev (gid=1024)"
group:
name: dev
gid: 1024
state: present
- name: "创建开发用户 dev (uid=1024)"
user:
name: dev
uid: 1024
group: dev
shell: /bin/bash
create_home: yes
state: present
# ---- NTP 时钟同步 ----
- name: "配置 NTP 上游服务器为腾讯云"
lineinfile:
path: /etc/chrony.conf
regexp: '^pool'
line: server ntp.tencent.com iburst
- name: "重启 chronyd 服务"
systemd:
name: chronyd
state: restarted
- name: "检查时钟同步状态"
shell: chronyc sources
register: ntp_result
- name: "打印时钟同步结果"
debug:
msg: "{{ ntp_result.stdout_lines }}"
序号任务模块说明
1拷贝 /etc/hostscopy将 ops-admin 的 hosts 分发到所有节点
2安装软件包package安装 tar、zip
3创建 dev 组groupgid=1024
4创建 dev 用户useruid=1024,shell=/bin/bash
5配置 NTPlineinfile上游服务器改为 ntp.tencent.com
6重启 chronydsystemd使 NTP 配置生效
7检查同步状态commandchronyc sources
8打印结果debug输出同步状态

时钟同步#

Terminal window
[root@ops-admin ~]# chronyc sources
MS Name/IP address Stratum Poll Reach LastRx Last sample
===============================================================================
^? 118.31.40.99 0 6 0 - +0ns[ +0ns] +/- 0ns
^? 118.31.3.89 0 6 0 - +0ns[ +0ns] +/- 0ns
^? 47.96.149.233 0 6 0 - +0ns[ +0ns] +/- 0ns
^? 116.62.13.223 0 6 0 - +0ns[ +0ns] +/- 0ns
# 所有源的状态都是 ^?(不可达/未同步)
[root@ops-admin ~]# egrep -v '^$|^#' /etc/chrony.conf
pool ntp1.aliyun.com iburst 'nts'
# 阿里云公共 NTP 服务器(ntp1.aliyun.com)不支持 NTS 协议
# 要去掉 nts 参数
sourcedir /run/chrony-dhcp
driftfile /var/lib/chrony/drift
makestep 1.0 3
rtcsync
ntsdumpdir /var/lib/chrony
logdir /var/log/chrony
[root@ops-admin ~]# sed -i '/^pool ntp1/c\server ntp.tencent.com iburst' /etc/chrony.conf
'如果原来有的话要注释掉原来的'
[root@ops-admin ~]# systemctl restart chronyd
[root@ops-admin ~]# chronyc sources
MS Name/IP address Stratum Poll Reach LastRx Last sample
===============================================================================
^* 106.55.184.199 2 6 13 0 +637us[ +246ms] +/- 70ms
`*` 当前 `最佳同步源`(被选中用来同步系统时间)
Terminal window
(.venv) [root@ops-admin Projects]# ansible-playbook playbook-init.yml
____________
< PLAY [all] >
------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
________________________
< TASK [Gathering Facts] >
------------------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
ok: [gitlab-server]
ok: [lb01]
ok: [waf01]
ok: [zabbix-server]
ok: [jumpserver]
ok: [web01]
ok: [web02]
ok: [share01]
__________________________________________
< TASK [拷贝 /etc/hosts 到所有节点] >
------------------------------------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
changed: [waf01]
changed: [jumpserver]
changed: [zabbix-server]
changed: [gitlab-server]
changed: [lb01]
changed: [web01]
changed: [web02]
changed: [share01]
______________________________
< TASK [安装必要软件包] >
------------------------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
changed: [jumpserver]
changed: [waf01]
changed: [lb01]
changed: [zabbix-server]
changed: [gitlab-server]
changed: [web01]
changed: [web02]
changed: [share01]
_______________________________________
< TASK [创建开发组 dev (gid=1024)] >
---------------------------------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
changed: [waf01]
changed: [gitlab-server]
changed: [jumpserver]
changed: [zabbix-server]
changed: [lb01]
changed: [web01]
changed: [web02]
changed: [share01]
__________________________________________
< TASK [创建开发用户 dev (uid=1024)] >
------------------------------------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
changed: [gitlab-server]
changed: [waf01]
changed: [zabbix-server]
changed: [jumpserver]
changed: [lb01]
changed: [web01]
changed: [web02]
changed: [share01]
_______________________________________________
< TASK [配置 NTP 上游服务器为腾讯云] >
-----------------------------------------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
changed: [gitlab-server]
changed: [jumpserver]
changed: [waf01]
changed: [zabbix-server]
changed: [lb01]
changed: [web01]
changed: [web02]
changed: [share01]
______________________________
< TASK [重启 chronyd 服务] >
------------------------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
changed: [zabbix-server]
changed: [waf01]
changed: [jumpserver]
changed: [lb01]
changed: [gitlab-server]
changed: [web01]
changed: [web02]
changed: [share01]
_________________________________
< TASK [检查时钟同步状态] >
---------------------------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
changed: [waf01]
changed: [gitlab-server]
changed: [jumpserver]
changed: [lb01]
changed: [zabbix-server]
changed: [web01]
changed: [web02]
changed: [share01]
_________________________________
< TASK [打印时钟同步结果] >
---------------------------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
ok: [gitlab-server] => {
"msg": [
"MS Name/IP address Stratum Poll Reach LastRx Last sample",
"=========================================================",
"^* 106.55.184.199 2 6 1 1 -673ms 67ms"
]
}
ok: [zabbix-server] => {
"msg": [
"MS Name/IP address Stratum Poll Reach LastRx Last sample",
"=========================================================",
"^* 106.55.184.199 2 6 1 1 -673ms 67ms"
]
}
ok: [jumpserver] => {
"msg": [
"MS Name/IP address Stratum Poll Reach LastRx Last sample",
"=========================================================",
"^* 106.55.184.199 2 6 1 1 -673ms 67ms"
]
}
ok: [waf01] => {
"msg": [
"MS Name/IP address Stratum Poll Reach LastRx Last sample",
"=========================================================",
"^* 106.55.184.199 2 6 1 1 -673ms 67ms"
]
}
ok: [lb01] => {
"msg": [
"MS Name/IP address Stratum Poll Reach LastRx Last sample",
"=========================================================",
"^* 106.55.184.199 2 6 1 1 -673ms 67ms"
]
}
ok: [web01] => {
"msg": [
"MS Name/IP address Stratum Poll Reach LastRx Last sample",
"=========================================================",
"^* 106.55.184.199 2 6 1 1 -673ms 67ms"
]
}
ok: [web02] => {
"msg": [
"MS Name/IP address Stratum Poll Reach LastRx Last sample",
"=========================================================",
"^* 106.55.184.199 2 6 1 1 -673ms 67ms"
]
}
ok: [share01] => {
"msg": [
"MS Name/IP address Stratum Poll Reach LastRx Last sample",
"=========================================================",
"^* 106.55.184.199 2 6 1 1 -673ms 67ms"
]
}
____________
< PLAY RECAP >
------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
gitlab-server : ok=9 changed=7
jumpserver : ok=9 changed=7
lb01 : ok=9 changed=7
share01 : ok=9 changed=7
waf01 : ok=9 changed=7
web01 : ok=9 changed=7
web02 : ok=9 changed=7
zabbix-server : ok=9 changed=7

GitLab 代码仓库#

在gitlab-server安装极狐gitlab

节点:gitlab-server.example.com (10.2.1.20)
域名:gitlab-server.example.com
协议:HTTPS 自签名证书


环境要求与准备#

(1)主机名解析

/etc/hosts 已通过 Ansible playbook-init.yml 分发到所有节点,无需手动配置。 管理网 IP 解析:10.2.1.20 gitlab-server gitlab-server.example.com

验证:

Terminal window
ping -c1 gitlab-server.example.com
# 自己ping自己
64 bytes from gitlab-server (10.2.1.20): icmp_seq=1 ttl=64 time=0.025 ms

(2)添加官方仓库并安装

Terminal window
curl -s https://packages.gitlab.com/install/repositories/gitlab/gitlab-ce/script.rpm.sh | bash
dnf install -y gitlab-ce

(3)生成自签名证书

Terminal window
mkdir -p /etc/gitlab/ssl
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout /etc/gitlab/ssl/gitlab-server.example.com.key \
-out /etc/gitlab/ssl/gitlab-server.example.com.crt \
-subj "/C=CN/ST=Beijing/L=Beijing/O=Group2/CN=gitlab-server.example.com"

(4)配置 HTTPS

编辑 /etc/gitlab/gitlab.rb

cat > /etc/gitlab/gitlab.rb << 'EOF'
external_url "https://gitlab-server.example.com"
nginx["redirect_http_to_https"] = true
nginx["ssl_certificate"] = "/etc/gitlab/ssl/gitlab-server.example.com.crt"
nginx["ssl_certificate_key"] = "/etc/gitlab/ssl/gitlab-server.example.com.key"
EOF

(5)应用配置(约 5-10 分钟)

Terminal window
gitlab-ctl reconfigure

(6)获取初始 root 密码

Terminal window
cat /etc/gitlab/initial_root_password
# Password: O/WfB1SI77/4WmHsY5OcyhqQxUzJkl4iai5GvVObUIQ=
jiuzhao@Ubuntu ~$ sudo vim /etc/hosts
10.0.0.20 gitlab-server.example.com
# 添加这个解析
https://gitlab-server.example.com

初始化配置#

(1)修改 root 密码

Terminal window
gitlab-rails runner "
u = User.find_by_username('root')
u.password = 'redhat123'
u.password_confirmation = 'redhat123'
u.save!
"

(2)创建普通用户 devuser

Terminal window
gitlab-rails runner "
org = Organizations::Organization.default_organization
u = User.new(
username: 'devuser',
email: 'developer@localhost.com',
name: 'devuser',
password: 'redhat123',
password_confirmation: 'redhat123',
confirmed_at: Time.now,
skip_confirmation: true,
organization_id: org.id
)
ns = Namespace.new(
name: 'devuser',
path: 'devuser',
type: 'User',
owner: u,
organization: org
)
u.namespace = ns
u.save!
puts 'devuser created'
"

(3)创建项目并添加成员

Terminal window
gitlab-rails runner "
# 以 devuser 创建项目
dev = User.find_by_username('devuser')
project = Projects::CreateService.new(
dev,
name: 'automation_project',
path: 'automation_project',
visibility_level: Gitlab::VisibilityLevel::PRIVATE
).execute
puts project.full_path
"

Ansible 剧本 — 安装 Git#

📌 ops-admin 本机

剧本路径:/Projects/playbook-git.yml
说明:ops-admin 作为 Ansible 控制节点,本身也要安装 git 才能推送代码。因 ops-admin 不在 inventory 中,使用 hosts: localhost + connection: local 在本机执行。

- hosts: localhost
gather_facts: yes
tasks:
- name: "安装 git"
package:
name: git
state: present
- name: "配置 git 用户名"
# git_config 是 ansible 内置模块,等价于: git config --global user.name "root"
git_config:
name: user.name
value: "root"
scope: global
- name: "配置 git 邮箱"
# 等价于: git config --global user.email "root@ops-admin.example.com"
git_config:
name: user.email
value: "root@ops-admin.example.com"
scope: global
- name: "跳过 SSL 验证(自签证书用)"
# 等价于: git config --global http.sslVerify false
git_config:
name: http.sslVerify
value: "false"
scope: global

执行:

Terminal window
cd /Projects && source venv/bin/activate
ansible-playbook playbook-git.yml

推送代码到 GitLab#

GitLab Web 页面https://gitlab-server.example.com(HTTPS 443 端口,非 8888)
自签证书浏览器会报警告,点”高级 → 继续访问”即可

Tip

GitLab CE 自带中文,登录后在右上角头像 → Preferences → Localization → Language → 简体中文。但是部分页面还是英文

Terminal window
'在ops-admin中操作即可'
cd /Projects
# 进入本地项目目录
git config --global init.defaultBranch main
# 设默认分支为 main(消除 init 时的提示)
git init
# 把当前目录变成 git 仓库(不再是 master 而是 main)
(.venv) [root@ops-admin Projects]# ls -lha | grep git
drwxr-xr-x 6 root root 103 Jun 30 16:30 .git
# 如果没设上面那个全局配置,就 init 后手动改名:
# git branch -m master main
echo "# automation_project" > README.md
# 随便写个文件
git add README.md
# 告诉 git:"这个文件我要提交"
git commit -m "init: 初始化自动化项目"
# 确认提交(拍个快照),加个备注
# 1 file changed, 1 insertion(+)
# create mode 100644 README.md
git -c http.sslVerify=false push \
https://root:redhat123@gitlab-server.example.com/root/automation_project.git main
# 推送至远程仓库,自签证书跳过 SSL 验证
# - https 开头 = HTTPS 协议
# - root:redhat123 = 用户名:密码
# - gitlab-server.example.com = 目标主机
# - /root/automation_project.git = 远程仓库路径
# - main = 推送的分支名
# -c 跳过证书校验(因为是自签名的,不受信任)
# 推送到远程(自签证书跳过验证)
https://root:redhat123@gitlab-server.example.com/root/automation_project.git main
Enumerating objects: 3, done.
Counting objects: 100% (3/3), done.
Writing objects: 100% (3/3), 267 bytes | 267.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
To https://gitlab-server.example.com/root/automation_project.git
* [new branch] main -> main
改成这样效果
https://root:redhat123@以 root(管理员)推送
https://devuser:redhat123@以 devuser(普通用户)推送

Lb负载&Web服务#

基础环境准备#

Terminal window
(.venv) [root@ops-admin Projects]# mkdir nginx
(.venv) [root@ops-admin Projects]# cat > ./nginx/nginx.conf <<'EOF'
user www;
worker_processes auto;
worker_cpu_affinity auto; # cpu亲和
pid /run/nginx.pid;
error_log /var/log/nginx/error.log notice;
worker_rlimit_nofile 65535; # 每个work能打开的文件描述符
events {
worker_connections 32768;
# 每个worker进程可以同时处理最大连接数
# 一个连接平均占用两个文件描述符
multi_accept on;
use epoll;
}
http {
include mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
server_tokens off; # 禁止浏览器显示nginx版本号
client_max_body_size 20m; # 文件上传大小限制调整 默认1M
sendfile on;
tcp_nopush on;
# tcp_nodelay on;
keepalive_timeout 65;
# Gzip
gzip on;
# 小于1KB的文件不压缩(避免小文件压缩后反而变大)
gzip_min_length 1k;
gzip_comp_level 6; # 压缩等级6(通用)
gzip_types text/plain application/javascript text/css application/xml application/json;
# 压缩的类型:必须用空格隔开
# 子配置目录
include /etc/nginx/conf.d/*.conf;
}
EOF
======================================
(.venv) [root@ops-admin Projects]# cat > ./nginx/www.conf <<'EOF'
[www]
user = www
group = www
listen = 127.0.0.1:9000
listen.acl_users = apache,nginx
listen.allowed_clients = 127.0.0.1
pm = dynamic
pm.max_children = 50
pm.start_servers = 5
pm.min_spare_servers = 5
pm.max_spare_servers = 35
slowlog = /var/log/php-fpm/www-slow.log
php_admin_value[error_log] = /var/log/php-fpm/www-error.log
php_admin_flag[log_errors] = on
php_value[session.save_handler] = files
php_value[session.save_path] = /var/lib/php/session
php_value[soap.wsdl_cache_dir] = /var/lib/php/wsdlcache
EOF
======================================
(.venv) [root@ops-admin Projects]# cat > ./nginx/php.ini <<'EOF'
[PHP]
engine = On
short_open_tag = Off
precision = 14
output_buffering = 4096
zlib.output_compression = Off
implicit_flush = Off
unserialize_callback_func =
serialize_precision = -1
disable_functions =
disable_classes =
zend.enable_gc = On
zend.exception_ignore_args = On
zend.exception_string_param_max_len = 0
expose_php = On
max_execution_time = 300
max_input_time = 300
memory_limit = 256M
error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT
display_errors = Off
display_startup_errors = Off
log_errors = On
ignore_repeated_errors = Off
ignore_repeated_source = Off
report_memleaks = On
variables_order = "GPCS"
request_order = "GP"
register_argc_argv = Off
auto_globals_jit = On
post_max_size = 64M
auto_prepend_file =
auto_append_file =
default_mimetype = "text/html"
default_charset = "UTF-8"
doc_root =
user_dir =
enable_dl = Off
file_uploads = On
upload_max_filesize = 64M
max_file_uploads = 20
allow_url_fopen = On
allow_url_include = Off
default_socket_timeout = 60
[CLI Server]
cli_server.color = On
[Date]
date.timezone = Asia/Shanghai
[filter]
[iconv]
[imap]
[intl]
[sqlite3]
[Pcre]
pcre.jit=0
[Pdo]
[Pdo_mysql]
pdo_mysql.default_socket=
[Phar]
[mail function]
sendmail_path = /usr/sbin/sendmail -t -i
mail.add_x_header = Off
mail.mixed_lf_and_crlf = Off
[ODBC]
odbc.allow_persistent = On
odbc.check_persistent = On
odbc.max_persistent = -1
odbc.max_links = -1
odbc.defaultlrl = 4096
odbc.defaultbinmode = 1
[MySQLi]
mysqli.max_persistent = -1
mysqli.allow_persistent = On
mysqli.max_links = -1
mysqli.default_port = 3306
mysqli.default_socket =
mysqli.default_host =
mysqli.default_user =
mysqli.default_pw =
[mysqlnd]
mysqlnd.collect_statistics = On
mysqlnd.collect_memory_statistics = Off
[PostgreSQL]
pgsql.allow_persistent = On
pgsql.auto_reset_persistent = Off
pgsql.max_persistent = -1
pgsql.max_links = -1
pgsql.ignore_notice = 0
pgsql.log_notice = 0
[bcmath]
bcmath.scale = 0
[browscap]
[Session]
session.save_handler = files
session.use_strict_mode = 0
session.use_cookies = 1
session.use_only_cookies = 1
session.name = PHPSESSID
session.auto_start = 0
session.cookie_lifetime = 0
session.cookie_path = /
session.cookie_domain =
session.cookie_httponly =
session.cookie_samesite =
session.serialize_handler = php
session.gc_probability = 1
session.gc_divisor = 1000
session.gc_maxlifetime = 1440
session.referer_check =
session.cache_limiter = nocache
session.cache_expire = 180
session.use_trans_sid = 0
session.sid_length = 26
session.trans_sid_tags = "a=href,area=href,frame=src,form="
session.sid_bits_per_character = 5
[Assertion]
zend.assertions = -1
[mbstring]
[gd]
[exif]
[Tidy]
tidy.clean_output = Off
[soap]
soap.wsdl_cache_enabled=1
soap.wsdl_cache_dir="/tmp"
soap.wsdl_cache_ttl=86400
soap.wsdl_cache_limit = 5
[sysvshm]
[ldap]
ldap.max_links = -1
[dba]
[opcache]
[curl]
[openssl]
[ffi]
EOF
======================================
(.venv) [root@ops-admin Projects]# cat > ./nginx/default.conf.j2 <<'EOF'
server {
listen {{ansible_ens2.ipv4.address}}:8000 default_server;
server_name _;
root /usr/share/nginx/html;
location / {
index index.html index.php;
# 先静态后PHP
}
error_page 500 502 503 504 /50x.html;
location ~ \.php$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}
EOF
======================================
(.venv) [root@ops-admin Projects]# cat > ./nginx/proxy_set <<'EOF'
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_http_version 1.1;
proxy_connect_timeout 30;
proxy_send_timeout 60;
proxy_read_timeout 60;
proxy_buffering on;
proxy_buffer_size 32k;
proxy_buffers 4 128k;
EOF
======================================
(.venv) [root@ops-admin Projects]# cat > ./nginx/lb.conf <<'EOF'
upstream webs {
server 10.2.2.71:8000;
server 10.2.2.72:8000;
}
server {
listen 10.2.2.60:80;
server_name www.wp.com;
location / {
proxy_pass http://webs;
include proxy_set;
}
}
EOF

index.html#

Terminal window
(.venv) [root@ops-admin Projects]# vim ./nginx/index.html.j2
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.5, user-scalable=yes">
<title>web默认页 - {{ ansible_hostname }}</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: linear-gradient(145deg, #f0f4f8 0%, #e9eff5 100%);
font-family: 'Segoe UI', 'Roboto', system-ui, sans-serif;
min-height: 100vh; display: flex; justify-content: center; align-items: center;
padding: 2rem 1rem; color: #1e293b;
}
.dashboard {
max-width: 1100px; width: 100%; background: #ffffff;
box-shadow: 0 25px 50px -12px rgba(0,0,0,0.15);
border-radius: 2.5rem; padding: 2.5rem;
}
@media (max-width: 640px) { .dashboard { padding: 1.8rem; border-radius: 2rem; } }
.header {
display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between;
margin-bottom: 2rem; border-bottom: 2px dashed #cbd5e1; padding-bottom: 1.2rem;
}
.host-badge { display: flex; flex-wrap: wrap; align-items: center; gap: 1.2rem; }
.hostname {
font-size: 2.5rem; font-weight: 700;
background: linear-gradient(135deg, #0f2b5c, #1e4b6e);
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
background-clip: text; letter-spacing: -0.5px;
}
.ip-address {
background: #e0e7ff; padding: 0.5rem 1.3rem; border-radius: 3rem;
font-weight: 600; font-size: 1.1rem; color: #1e3a8a;
display: inline-flex; align-items: center; gap: 0.3rem;
}
.ip-address::before { content: "🌐"; }
.php-status {
background: #f1f5f9; border-radius: 2rem; padding: 0.4rem 1.4rem;
font-weight: 600; display: flex; align-items: center; gap: 0.4rem;
}
.badge-success { background: #dcfce7; color: #166534; border-left: 4px solid #22c55e; }
.badge-fail { background: #fee2e2; color: #991b1b; border-left: 4px solid #ef4444; }
.grid-main {
display: grid; grid-template-columns: 1fr 1fr; gap: 2rem;
margin: 2rem 0 2.2rem;
}
@media (max-width: 750px) { .grid-main { grid-template-columns: 1fr; } }
.card {
background: #f8fafc; border-radius: 1.8rem; padding: 1.8rem;
box-shadow: 0 10px 25px -8px rgba(0,0,0,0.08); border: 1px solid #e9eef2;
transition: transform 0.2s;
}
.card:hover { transform: translateY(-3px); box-shadow: 0 20px 30px -12px rgba(0,0,0,0.12); }
.card h3 { font-size: 1.5rem; font-weight: 600; margin-bottom: 1.3rem; display: flex; align-items: center; gap: 0.5rem; color: #0c4a6e; }
.upload-area {
border: 2px dashed #94a3b8; border-radius: 1.2rem; padding: 2rem 1rem;
text-align: center; background: #ffffff; cursor: pointer; transition: 0.25s; margin-bottom: 1.2rem;
}
.upload-area:hover { border-color: #2563eb; background: #f0f4ff; }
#file-input { display: none; }
.preview-img {
max-width: 100%; max-height: 150px; margin-top: 1rem;
border-radius: 1rem; box-shadow: 0 8px 18px rgba(0,0,0,0.1); display: none;
}
.btn {
background: #f1f5f9; border: 1px solid #cbd5e1; padding: 0.7rem 1.8rem;
border-radius: 2.5rem; font-weight: 600; cursor: pointer; transition: 0.2s;
display: inline-flex; align-items: center; gap: 0.3rem; font-size: 0.95rem;
}
.btn-primary {
background: linear-gradient(135deg, #1e293b, #0f172a); color: white; border: none;
box-shadow: 0 8px 18px rgba(15,23,42,0.3);
}
.btn-primary:hover { background: #1e293b; }
.btn-danger {
background: #dc2626; color: white; border: none; margin-left: 0.5rem;
}
.btn-danger:hover { background: #b91c1c; }
.db-panel { display: flex; flex-direction: column; gap: 1rem; }
.form-row { display: flex; flex-wrap: wrap; align-items: center; gap: 0.8rem; }
.form-row label { font-weight: 600; min-width: 55px; color: #334155; }
input, select {
background: white; border: 1px solid #d1d5db; border-radius: 2rem;
padding: 0.7rem 1.2rem; font-size: 0.95rem; flex: 1; outline: none;
transition: 0.2s; font-family: inherit;
}
input:focus, select:focus { border-color: #2563eb; box-shadow: 0 0 0 3px rgba(37,99,235,0.2); }
.db-result, .upload-status { margin-top: 0.5rem; font-weight: 600; }
.file-info { margin-top: 1rem; background: #f1f5f9; padding: 0.8rem; border-radius: 1rem; display: none; }
.file-info p { margin: 0.3rem 0; font-size: 0.9rem; }
/* 图片画廊 */
.gallery-title { margin-top: 1.8rem; margin-bottom: 0.8rem; font-weight: 600; color: #334155; }
.gallery {
display: flex; flex-wrap: wrap; gap: 0.8rem; max-height: 200px; overflow-y: auto;
padding: 0.5rem; background: #ffffff; border-radius: 1rem; border: 1px solid #e2e8f0;
}
.gallery-item {
width: 80px; height: 80px; border-radius: 0.8rem; overflow: hidden;
cursor: pointer; border: 2px solid transparent; transition: 0.2s;
position: relative; flex-shrink: 0;
}
.gallery-item img {
width: 100%; height: 100%; object-fit: cover;
}
.gallery-item:hover { border-color: #2563eb; transform: scale(1.05); }
.gallery-item .delete-icon {
position: absolute; top: 2px; right: 2px; background: rgba(0,0,0,0.6);
color: white; border-radius: 50%; width: 20px; height: 20px;
display: flex; align-items: center; justify-content: center;
font-size: 12px; display: none;
}
.gallery-item:hover .delete-icon { display: flex; }
/* 详情弹窗 */
.modal {
display: none; position: fixed; z-index: 100; left: 0; top: 0;
width: 100%; height: 100%; background: rgba(0,0,0,0.6);
justify-content: center; align-items: center;
}
.modal.active { display: flex; }
.modal-content {
background: white; border-radius: 2rem; padding: 2rem;
max-width: 500px; width: 90%; text-align: center;
position: relative; animation: fadeIn 0.3s;
}
@keyframes fadeIn { from { opacity: 0; transform: translateY(-20px); } to { opacity: 1; transform: translateY(0); } }
.modal img { max-width: 100%; max-height: 250px; border-radius: 1rem; margin-bottom: 1rem; }
.modal .detail-row { display: flex; justify-content: space-between; margin: 0.5rem 0; font-size: 0.95rem; }
.close-btn {
position: absolute; top: 1rem; right: 1.5rem; font-size: 1.8rem;
cursor: pointer; color: #64748b;
}
hr { margin: 1.5rem 0 1rem; border: 0.5px solid #e2e8f0; }
.footer-note { text-align: right; font-size: 0.9rem; color: #64748b; margin-top: 1rem; }
button:disabled { opacity: 0.6; cursor: not-allowed; }
</style>
</head>
<body>
<div class="dashboard">
<div class="header">
<div class="host-badge">
<span class="hostname">{{ ansible_hostname }}</span>
<span class="ip-address">{{ ansible_ens2.ipv4.address }}</span>
</div>
<div id="php-indicator" class="php-status badge-success"></div>
</div>
<div class="grid-main">
<!-- 图片管理卡片 -->
<div class="card">
<h3><span>📁</span> 图片管理</h3>
<!-- 上传区域 -->
<div id="upload-box" class="upload-area">
<div class="upload-icon">📤</div>
<p><strong>点击或拖拽上传</strong></p>
<p style="font-size:0.8rem; color:#64748b;">JPG, PNG, GIF</p>
<input type="file" id="file-input" accept="image/*">
<img id="preview-image" class="preview-img" alt="预览">
</div>
<div style="display: flex; justify-content: space-between; align-items: center;">
<button id="upload-btn" class="btn btn-primary">⬆️ 上传</button>
<span id="upload-message" class="upload-status"></span>
</div>
<!-- 已上传图片列表 -->
<div class="gallery-title">🖼️ 已上传图片</div>
<div id="gallery" class="gallery">
<!-- 动态填充缩略图 -->
</div>
</div>
<!-- 数据库卡片保持不变 -->
<div class="card">
<h3><span>🗄️</span> 数据库连通性测试</h3>
<div class="db-panel">
<div class="form-row">
<label>主机</label>
<input type="text" id="db-host" placeholder="192.168.1.100" value="192.168.1.100">
</div>
<div class="form-row">
<label>端口</label>
<input type="number" id="db-port" placeholder="3306" value="3306">
</div>
<div class="form-row">
<label>用户</label>
<input type="text" id="db-user" placeholder="root" value="root">
</div>
<div class="form-row">
<label>密码</label>
<input type="password" id="db-pass" placeholder="数据库密码">
</div>
<div class="form-row">
<label>库名</label>
<input type="text" id="db-name" placeholder="test" value="test">
</div>
<button id="test-db-btn" class="btn btn-primary" style="justify-content:center;">🔌 测试连接</button>
<div id="db-result" class="db-result"></div>
<div id="tables-info" style="display:none; margin-top:0.3rem;">
<div id="tables-count" style="font-weight:600;"></div>
<div id="tables-list" class="tables-list"></div>
</div>
</div>
</div>
</div>
<hr>
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap;">
<div style="display: flex; gap: 1.5rem;">
<span><strong>PHP状态:</strong><span id="php-text">检测中</span></span>
<span><strong>服务器时间:</strong><span id="server-time">--</span></span>
</div>
<div class="footer-note">Ansible 部署 • {{ ansible_hostname }}</div>
</div>
</div>
<!-- 图片详情模态框 -->
<div id="imageModal" class="modal">
<div class="modal-content">
<span class="close-btn" id="closeModal">&times;</span>
<img id="modal-img" src="" alt="大图">
<div class="detail-row"><strong>文件名:</strong><span id="modal-name"></span></div>
<div class="detail-row"><strong>大小:</strong><span id="modal-size"></span></div>
<div class="detail-row"><strong>修改时间:</strong><span id="modal-mtime"></span></div>
<div class="detail-row"><strong>属主:</strong><span id="modal-owner"></span></div>
<div style="margin-top:1.5rem; display: flex; justify-content: center; gap:1rem;">
<a id="modal-link" href="#" target="_blank" class="btn">🔍 查看原图</a>
<button id="modal-delete" class="btn btn-danger">🗑️ 删除图片</button>
</div>
</div>
</div>
<script>
// ---------- 全局 DOM ----------
const uploadBox = document.getElementById('upload-box');
const fileInput = document.getElementById('file-input');
const previewImg = document.getElementById('preview-image');
const uploadBtn = document.getElementById('upload-btn');
const uploadMessage = document.getElementById('upload-message');
const galleryDiv = document.getElementById('gallery');
const phpIndicator = document.getElementById('php-indicator');
const phpTextSpan = document.getElementById('php-text');
const serverTimeSpan = document.getElementById('server-time');
const testDbBtn = document.getElementById('test-db-btn');
const dbResultDiv = document.getElementById('db-result');
const tablesInfoDiv = document.getElementById('tables-info');
const tablesCountSpan = document.getElementById('tables-count');
const tablesListDiv = document.getElementById('tables-list');
// 模态框元素
const modal = document.getElementById('imageModal');
const closeModal = document.getElementById('closeModal');
const modalImg = document.getElementById('modal-img');
const modalName = document.getElementById('modal-name');
const modalSize = document.getElementById('modal-size');
const modalMtime = document.getElementById('modal-mtime');
const modalOwner = document.getElementById('modal-owner');
const modalLink = document.getElementById('modal-link');
const modalDelete = document.getElementById('modal-delete');
let selectedFile = null;
// 更新时间
function updateServerTime() {
serverTimeSpan.textContent = new Date().toLocaleString('zh-CN', { hour12: false });
}
updateServerTime();
setInterval(updateServerTime, 1000);
// PHP状态检测
async function checkPhpStatus() {
try {
const res = await fetch('index.php?action=phpinfo', { cache: 'no-store' });
if (res.ok) {
const data = await res.json();
if (data && data.php === 'active') {
phpIndicator.className = 'php-status badge-success';
phpIndicator.innerHTML = '✅ PHP 已连接';
phpTextSpan.textContent = '运行正常';
return true;
}
}
throw new Error('PHP异常');
} catch (e) {
phpIndicator.className = 'php-status badge-fail';
phpIndicator.innerHTML = '❌ PHP 未连接';
phpTextSpan.textContent = '不可用';
return false;
}
}
// ---------- 图片列表加载 ----------
async function loadImageList() {
try {
const res = await fetch('index.php?action=listimages');
const data = await res.json();
if (data.status === 'success' && Array.isArray(data.images)) {
renderGallery(data.images);
} else {
galleryDiv.innerHTML = '<span style="color:#64748b; font-size:0.9rem;">暂无图片</span>';
}
} catch (e) {
galleryDiv.innerHTML = '<span style="color:#ef4444;">加载图片列表失败</span>';
}
}
function renderGallery(images) {
if (images.length === 0) {
galleryDiv.innerHTML = '<span style="color:#64748b; font-size:0.9rem;">暂无图片</span>';
return;
}
galleryDiv.innerHTML = images.map(img => `
<div class="gallery-item" data-filename="${img.name}" onclick="openImageDetail('${img.name}')">
<img src="img/${img.name}" alt="${img.name}">
<span class="delete-icon" onclick="event.stopPropagation(); deleteImage('${img.name}')">✕</span>
</div>
`).join('');
}
// 打开详情弹窗
async function openImageDetail(filename) {
// 请求该图片的详细信息
try {
const res = await fetch(`index.php?action=imageinfo&file=${encodeURIComponent(filename)}`);
const data = await res.json();
if (data.status === 'success') {
modalImg.src = `img/${filename}`;
modalName.textContent = filename;
modalSize.textContent = data.size;
modalMtime.textContent = data.mtime;
modalOwner.textContent = data.owner;
modalLink.href = `img/${filename}`;
modalDelete.setAttribute('data-filename', filename);
modal.classList.add('active');
} else {
alert('获取图片信息失败');
}
} catch (e) {
alert('请求失败');
}
}
// 关闭弹窗
closeModal.addEventListener('click', () => modal.classList.remove('active'));
window.addEventListener('click', (e) => {
if (e.target === modal) modal.classList.remove('active');
});
// 删除图片
async function deleteImage(filename) {
if (!confirm(`确定要删除 ${filename} 吗?`)) return;
try {
const formData = new FormData();
formData.append('action', 'deleteimage');
formData.append('filename', filename);
const res = await fetch('index.php', { method: 'POST', body: formData });
const data = await res.json();
if (data.status === 'success') {
loadImageList();
// 如果模态框正显示该图片则关闭
if (modalDelete.getAttribute('data-filename') === filename) {
modal.classList.remove('active');
}
} else {
alert('删除失败: ' + data.message);
}
} catch (e) {
alert('删除请求异常');
}
}
// 模态框内删除按钮
modalDelete.addEventListener('click', () => {
const filename = modalDelete.getAttribute('data-filename');
if (filename) deleteImage(filename);
});
// ---------- 上传逻辑 ----------
uploadBox.addEventListener('click', () => fileInput.click());
uploadBox.addEventListener('dragover', e => {
e.preventDefault();
uploadBox.style.borderColor = '#2563eb';
uploadBox.style.background = '#f0f4ff';
});
uploadBox.addEventListener('dragleave', () => {
uploadBox.style.borderColor = '#94a3b8';
uploadBox.style.background = '#ffffff';
});
uploadBox.addEventListener('drop', e => {
e.preventDefault();
uploadBox.style.borderColor = '#94a3b8';
uploadBox.style.background = '#ffffff';
if (e.dataTransfer.files.length > 0) handleFileSelect(e.dataTransfer.files[0]);
});
fileInput.addEventListener('change', e => {
if (e.target.files.length > 0) handleFileSelect(e.target.files[0]);
});
function handleFileSelect(file) {
if (!file.type.startsWith('image/')) {
alert('请选择图片文件');
return;
}
selectedFile = file;
const reader = new FileReader();
reader.onload = e => {
previewImg.src = e.target.result;
previewImg.style.display = 'block';
};
reader.readAsDataURL(file);
uploadMessage.textContent = `已选择: ${file.name}`;
}
uploadBtn.addEventListener('click', async () => {
if (!selectedFile) {
uploadMessage.textContent = '⚠️ 请先选择图片';
return;
}
const formData = new FormData();
formData.append('image', selectedFile);
uploadBtn.disabled = true;
uploadMessage.textContent = '⏳ 上传中...';
try {
const res = await fetch('index.php?action=upload', { method: 'POST', body: formData });
const result = await res.json();
if (result.status === 'success') {
uploadMessage.textContent = '✅ 上传成功';
previewImg.style.display = 'none';
selectedFile = null;
fileInput.value = '';
loadImageList(); // 刷新列表
} else {
uploadMessage.textContent = `❌ ${result.message || '上传失败'}`;
}
} catch (err) {
uploadMessage.textContent = '❌ 网络错误或PHP未响应';
} finally {
uploadBtn.disabled = false;
}
});
// ---------- 数据库测试 ----------
testDbBtn.addEventListener('click', async () => {
const host = document.getElementById('db-host').value.trim();
const port = document.getElementById('db-port').value.trim();
const user = document.getElementById('db-user').value.trim();
const pass = document.getElementById('db-pass').value;
const dbname = document.getElementById('db-name').value.trim();
if (!host || !port || !user) {
dbResultDiv.innerHTML = '⚠️ 主机、端口、用户不能为空';
dbResultDiv.style.color = '#b45309';
tablesInfoDiv.style.display = 'none';
return;
}
dbResultDiv.innerHTML = '⏳ 正在测试连接...';
dbResultDiv.style.color = '#1e293b';
tablesInfoDiv.style.display = 'none';
testDbBtn.disabled = true;
try {
const formData = new FormData();
formData.append('action', 'dbtest');
formData.append('db_host', host);
formData.append('db_port', port);
formData.append('db_user', user);
formData.append('db_pass', pass);
formData.append('db_name', dbname);
const res = await fetch('index.php', { method: 'POST', body: formData });
const result = await res.json();
if (result.status === 'success') {
dbResultDiv.innerHTML = '✅ 数据库连接成功';
dbResultDiv.style.color = '#166534';
await fetchTableList(host, port, user, pass, dbname);
} else {
dbResultDiv.innerHTML = `❌ 连接失败: ${result.message || '未知错误'}`;
dbResultDiv.style.color = '#991b1b';
}
} catch (err) {
dbResultDiv.innerHTML = '❌ 请求异常,PHP可能未运行';
dbResultDiv.style.color = '#991b1b';
} finally {
testDbBtn.disabled = false;
}
});
async function fetchTableList(host, port, user, pass, dbname) {
tablesInfoDiv.style.display = 'none';
try {
const formData = new FormData();
formData.append('action', 'listtables');
formData.append('db_host', host);
formData.append('db_port', port);
formData.append('db_user', user);
formData.append('db_pass', pass);
formData.append('db_name', dbname);
const res = await fetch('index.php', { method: 'POST', body: formData });
const data = await res.json();
if (data.status === 'success') {
tablesCountSpan.textContent = `📊 当前数据库共有 ${data.table_count} 张表`;
tablesListDiv.innerHTML = data.tables && data.tables.length
? data.tables.map(t => `<span style="background:#e2e8f0; padding:0.2rem 0.6rem; border-radius:1rem; margin:0.2rem; display:inline-block;">${t}</span>`).join('')
: '(无表)';
tablesInfoDiv.style.display = 'block';
} else {
tablesInfoDiv.innerHTML = `<span style="color:#991b1b;">❌ 获取表信息失败: ${data.message}</span>`;
tablesInfoDiv.style.display = 'block';
}
} catch (e) {
tablesInfoDiv.innerHTML = '<span style="color:#991b1b;">❌ 查询表请求异常</span>';
tablesInfoDiv.style.display = 'block';
}
}
// 页面初始化
checkPhpStatus();
loadImageList();
</script>
</body>
</html>

index.php#

Terminal window
(.venv) [root@ops-admin Projects]# vim ./nginx/index.php
<?php
/**
* 运维默认页后端接口
* 支持:PHP状态、图片上传/列表/详情/删除、数据库连接/表查询
*/
header('Access-Control-Allow-Origin: *');
header('Content-Type: application/json; charset=utf-8');
$action = $_GET['action'] ?? $_POST['action'] ?? '';
switch ($action) {
case 'phpinfo':
echo json_encode(['php' => 'active', 'version' => PHP_VERSION]);
break;
case 'upload':
handleUpload();
break;
case 'listimages':
listImages();
break;
case 'imageinfo':
imageInfo();
break;
case 'deleteimage':
deleteImage();
break;
case 'dbtest':
testDatabaseConnection();
break;
case 'listtables':
listTables();
break;
default:
http_response_code(400);
echo json_encode(['status' => 'error', 'message' => '未知操作']);
}
/**
* 图片上传
*/
function handleUpload() {
$uploadDir = __DIR__ . '/img/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
if (!isset($_FILES['image'])) {
echo json_encode(['status' => 'error', 'message' => '没有收到文件']);
return;
}
$file = $_FILES['image'];
$allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
if (!in_array($file['type'], $allowedTypes)) {
echo json_encode(['status' => 'error', 'message' => '仅支持 JPG/PNG/GIF']);
return;
}
$ext = pathinfo($file['name'], PATHINFO_EXTENSION);
$newName = uniqid('img_', true) . '.' . $ext;
$destPath = $uploadDir . $newName;
if (!move_uploaded_file($file['tmp_name'], $destPath)) {
echo json_encode(['status' => 'error', 'message' => '文件移动失败,检查权限']);
return;
}
echo json_encode(['status' => 'success', 'message' => '上传成功', 'filename' => $newName]);
}
/**
* 列出所有图片(简单列表,含文件名)
*/
function listImages() {
$imgDir = __DIR__ . '/img/';
if (!is_dir($imgDir)) {
echo json_encode(['status' => 'success', 'images' => []]);
return;
}
$files = array_diff(scandir($imgDir), ['.', '..']);
$images = [];
foreach ($files as $file) {
$ext = strtolower(pathinfo($file, PATHINFO_EXTENSION));
if (in_array($ext, ['jpg', 'jpeg', 'png', 'gif'])) {
$images[] = ['name' => $file];
}
}
echo json_encode(['status' => 'success', 'images' => array_values($images)]);
}
/**
* 获取单张图片详细信息
*/
function imageInfo() {
$filename = $_GET['file'] ?? '';
if (empty($filename)) {
echo json_encode(['status' => 'error', 'message' => '缺少文件名']);
return;
}
// 安全检查:不允许路径遍历
if (strpos($filename, '/') !== false || strpos($filename, '\\') !== false || $filename === '.' || $filename === '..') {
echo json_encode(['status' => 'error', 'message' => '非法的文件名']);
return;
}
$filePath = __DIR__ . '/img/' . $filename;
if (!file_exists($filePath)) {
echo json_encode(['status' => 'error', 'message' => '文件不存在']);
return;
}
$size = filesize($filePath);
$mtime = date("Y-m-d H:i:s", filemtime($filePath));
$owner = 'unknown';
if (function_exists('posix_getpwuid')) {
$ownerInfo = posix_getpwuid(fileowner($filePath));
$owner = $ownerInfo ? $ownerInfo['name'] : 'unknown';
}
echo json_encode([
'status' => 'success',
'size' => formatBytes($size),
'mtime' => $mtime,
'owner' => $owner
]);
}
/**
* 删除图片
*/
function deleteImage() {
$filename = $_POST['filename'] ?? '';
if (empty($filename)) {
echo json_encode(['status' => 'error', 'message' => '缺少文件名']);
return;
}
// 路径遍历检查
if (strpos($filename, '/') !== false || strpos($filename, '\\') !== false || $filename === '.' || $filename === '..') {
echo json_encode(['status' => 'error', 'message' => '非法的文件名']);
return;
}
$filePath = __DIR__ . '/img/' . $filename;
if (!file_exists($filePath)) {
echo json_encode(['status' => 'error', 'message' => '文件不存在']);
return;
}
if (unlink($filePath)) {
echo json_encode(['status' => 'success', 'message' => '删除成功']);
} else {
echo json_encode(['status' => 'error', 'message' => '删除失败,权限不足']);
}
}
// 以下为原有数据库相关函数(略作保留,内容不变)
function testDatabaseConnection() {
$host = $_POST['db_host'] ?? '';
$port = $_POST['db_port'] ?? '3306';
$user = $_POST['db_user'] ?? '';
$pass = $_POST['db_pass'] ?? '';
$dbname = $_POST['db_name'] ?? '';
if (empty($host) || empty($user)) {
echo json_encode(['status' => 'error', 'message' => '主机和用户不能为空']);
return;
}
try {
$dsn = "mysql:host=$host;port=$port;dbname=$dbname;charset=utf8mb4";
$pdo = new PDO($dsn, $user, $pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_TIMEOUT => 3
]);
echo json_encode(['status' => 'success', 'message' => '数据库连接成功']);
} catch (PDOException $e) {
echo json_encode(['status' => 'error', 'message' => $e->getMessage()]);
}
}
function listTables() {
$host = $_POST['db_host'] ?? '';
$port = $_POST['db_port'] ?? '3306';
$user = $_POST['db_user'] ?? '';
$pass = $_POST['db_pass'] ?? '';
$dbname = $_POST['db_name'] ?? '';
if (empty($host) || empty($user) || empty($dbname)) {
echo json_encode(['status' => 'error', 'message' => '参数不完整']);
return;
}
try {
$dsn = "mysql:host=$host;port=$port;dbname=$dbname;charset=utf8mb4";
$pdo = new PDO($dsn, $user, $pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_TIMEOUT => 3
]);
$stmt = $pdo->query("SHOW TABLES");
$tables = $stmt->fetchAll(PDO::FETCH_COLUMN);
echo json_encode([
'status' => 'success',
'table_count' => count($tables),
'tables' => $tables
]);
} catch (PDOException $e) {
echo json_encode(['status' => 'error', 'message' => $e->getMessage()]);
}
}
function formatBytes($bytes, $precision = 2) {
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
$bytes = max($bytes, 0);
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
$pow = min($pow, count($units) - 1);
return round($bytes / (1024 ** $pow), $precision) . ' ' . $units[$pow];
}

playbook#

Terminal window
(.venv) [root@ops-admin Projects]# vim playbook-nginx.yml
- hosts: lb01,web01,web02
tasks:
- name: "配置nginx的源"
yum_repository:
name: nginx-stable
description: nginx stable repo
baseurl: https://nginx.org/packages/centos/$releasever/$basearch/
gpgcheck: no
enabled: yes
- name: "安装nginx"
package:
name: nginx
state: present
- name: "创建www组"
group:
name: www
gid: 888
- name: "创建www用户"
user:
name: www
uid: 888
group: www
shell: /sbin/nologin
create_home: false
- name: "配置Nginx的主配置文件"
copy:
src: ./nginx/nginx.conf
dest: /etc/nginx/
- name: "启动nginx"
systemd:
name: nginx
state: started
enabled: yes
- hosts:
- web01
- web02
tasks:
- name: "安装php服务"
package:
name:
- php
- php-bcmath
- php-cli
- php-common
- php-devel
- php-embedded
- php-fpm
- php-gd
- php-intl
- php-mbstring
- php-mysqlnd
- php-opcache
- php-pdo
- php-process
- php-xml
- php-json
state: present
- name: "拷贝配置文件"
copy:
src: "{{ item.src }}"
dest: "{{ item.dest }}"
loop:
- src: ./nginx/www.conf
dest: /etc/php-fpm.d/
- src: ./nginx/php.ini
dest: /etc/
- src: ./nginx/index.php
dest: /usr/share/nginx/html
- name: "启动php服务"
systemd:
name: php-fpm
state: started
enabled: yes
- name: "拷贝index.html和default.conf"
template:
src: "{{ item.src }}"
dest: "{{ item.dest }}"
loop:
- src: ./nginx/default.conf.j2
dest: /etc/nginx/conf.d/default.conf
- src: ./nginx/index.html.j2
dest: /usr/share/nginx/html/index.html
- name: "创建img目录存在"
file:
path: /usr/share/nginx/html/img
state: directory
owner: www
group: www
mode: '0755'
- name: "递归修改站点目录属主"
file:
path: /usr/share/nginx/html
state: directory
owner: www
group: www
recurse: yes
- hosts: lb01
tasks:
- name: "拷贝lb配置文件"
copy:
src: "{{ item.src }}"
dest: "{{ item.dest }}"
loop:
- src: ./nginx/lb.conf
dest: /etc/nginx/conf.d/
- src: ./nginx/proxy_set
dest: /etc/nginx/
- hosts: lb01,web01,web02
tasks:
- name: "重启nginx服务"
systemd:
name: nginx
state: restarted
Terminal window
(.venv) [root@ops-admin Projects]# ansible-playbook playbook-nginx.yml

share共享存储#

  • 在share01节点部署mysql和nfs
  • 在mysql中创建测试库表
  • 数据库不允许匿名访问, 设置root密码为Redhat@123,创建php用于连接的用户webuser,设置密码为Redhat@123
  • 在nfs中创建共享目录/webshare,共享web页面(test.html、test.php)
  • test.php用于展示mysql测试库表的内容
  • 将nfs的共享目录挂载到web01和web02的web根目录/webdir
  • web01和web02与share01的服务通信(php-fpm和nfs)使用ens3网卡

基础配置#

Terminal window
(.venv) [root@ops-admin Projects]# mkdir -p share
(.venv) [root@ops-admin Projects]# cat > ./share/exports.j2 <<'EOF'
{{dir}} 10.2.3.0/24(rw,sync,all_squash,anonuid={{uid}},anongid={{uid}})
{{wpdir}} 10.2.3.0/24(rw,sync,all_squash,anonuid={{uid}},anongid={{uid}})
EOF

mysql安装脚本#

Terminal window
(.venv) [root@ops-admin sh]# pwd
/server/sh
(.venv) [root@ops-admin sh]# vim install_mysql.sh
#!/bin/bash
# ============================================================
# 脚本功能:在 Rocky Linux 9 上通过本地 RPM 包离线安装 MySQL 8.0.36
# 适用场景:已将 mysql-8.0.36-1.el9.x86_64.rpm-bundle.tar 解压到 /tmp 目录
# ============================================================
# ------ 颜色定义(方便提示) ------
RED='\033[31m'
GREEN='\033[32m'
YELLOW='\033[33m'
BLUE='\033[34m'
NC='\033[0m' # 无颜色
# ------ 检查是否以 root 运行 ------
if [ "$EUID" -ne 0 ]; then
echo -e "${RED}错误:请使用 root 用户执行此脚本!${NC}"
exit 1
fi
# ------ 检查是否已安装 MySQL ------
if rpm -q mysql-community-server &>/dev/null; then
echo -e "${YELLOW}警告:检测到 MySQL 已安装,为避免冲突,脚本退出。${NC}"
echo -e "${YELLOW}如需重新安装,请先卸载现有版本。${NC}"
exit 0
fi
# ------ 定义变量 ------
RPM_DIR="/tmp"
LOG_FILE="/var/log/mysqld.log"
TEMP_PASS=""
NEW_PASS="passwd" # 最终设置的 root 密码
DB_NAME="wp"
DB_USER="wpuser"
DB_PASS="passwd"
echo -e "${BLUE}>>> 步骤1:切换到 RPM 包目录 ${RPM_DIR}${NC}"
cd "$RPM_DIR" || { echo -e "${RED}错误:无法进入目录 $RPM_DIR${NC}"; exit 1; }
# ------ 安装 RPM 包(使用 dnf 自动解决依赖) ------
echo -e "${BLUE}>>> 步骤2:安装 MySQL 8.0.36 RPM 包${NC}"
dnf install -y \
mysql-community-common-8.0.36-1.el9.x86_64.rpm \
mysql-community-icu-data-files-8.0.36-1.el9.x86_64.rpm \
mysql-community-libs-8.0.36-1.el9.x86_64.rpm \
mysql-community-client-plugins-8.0.36-1.el9.x86_64.rpm \
mysql-community-client-8.0.36-1.el9.x86_64.rpm \
mysql-community-server-8.0.36-1.el9.x86_64.rpm \
mysql-community-devel-8.0.36-1.el9.x86_64.rpm
if [ $? -ne 0 ]; then
echo -e "${RED}错误:RPM 安装失败,请检查包是否完整。${NC}"
exit 1
fi
echo -e "${GREEN}✓ RPM 安装完成${NC}"
# ------ 清理安装包(删除 /tmp 下所有 .rpm 文件) ------
echo -e "${BLUE}>>> 步骤3:删除临时 RPM 文件${NC}"
ls | grep -v mysql-community-server-8.0.36-1.el9.x86_64.rpm | xargs rm -rf "$RPM_DIR"/*.rpm
echo -e "${GREEN}✓ 清理完成${NC}"
# ------ 启动 MySQL 服务 ------
echo -e "${BLUE}>>> 步骤4:启动 mysqld 并设置开机自启${NC}"
systemctl enable --now mysqld &>/dev/null
if [ $? -ne 0 ]; then
echo -e "${RED}错误:MySQL 服务启动失败,请检查日志 $LOG_FILE${NC}"
exit 1
fi
echo -e "${GREEN}✓ MySQL 服务已启动${NC}"
# ------ 等待服务完全就绪(避免日志尚未生成) ------
sleep 3
# ------ 从日志中提取临时密码 ------
echo -e "${BLUE}>>> 步骤5:获取 root 临时密码${NC}"
TEMP_PASS=$(grep 'temporary password' "$LOG_FILE" | awk '{print $NF}')
if [ -z "$TEMP_PASS" ]; then
echo -e "${RED}错误:未找到临时密码,请检查 $LOG_FILE${NC}"
exit 1
fi
echo -e "${GREEN}✓ 临时密码获取成功${NC}"
# ------ 连接数据库并执行初始化 SQL ------
echo -e "${BLUE}>>> 步骤6:修改密码策略并配置数据库${NC}"
# 注意:临时密码可能包含特殊字符,使用双引号包裹,并用 --connect-expired-password 选项
mysql --connect-expired-password -uroot -p"$TEMP_PASS" <<EOF
-- 1. 先修改 root 密码为复杂密码(必须符合默认策略:长度≥8,含大小写字母、数字、特殊字符)
ALTER USER 'root'@'localhost' IDENTIFIED BY 'Oldboy123.com';
-- 2. 调整密码策略(允许简单密码)
SET GLOBAL validate_password.length = 6;
SET GLOBAL validate_password.policy = 0;
-- 3. 修改 root 密码为 'passwd'
ALTER USER 'root'@'localhost' IDENTIFIED BY '${NEW_PASS}';
-- 4. 设置全局时区(东八区)
SET GLOBAL time_zone = '+8:00';
-- 5. 创建数据库(指定字符集)
CREATE DATABASE IF NOT EXISTS ${DB_NAME} CHARACTER SET utf8 COLLATE utf8_bin;
-- 6. 创建用户(允许任何主机连接,生产环境请按需修改)
CREATE USER IF NOT EXISTS '${DB_USER}'@'%' IDENTIFIED BY '${DB_PASS}';
-- 7. 使用 mysql_native_password 插件(兼容旧客户端)
ALTER USER '${DB_USER}'@'%' IDENTIFIED WITH mysql_native_password BY '${DB_PASS}';
-- 8. 授权:授予 ${DB_NAME} 库的所有权限
GRANT ALL PRIVILEGES ON ${DB_NAME}.* TO '${DB_USER}'@'%';
-- 9. 允许普通用户创建函数和触发器(Zabbix 等可能需要)
SET GLOBAL log_bin_trust_function_creators = 1;
-- 10. 刷新权限
FLUSH PRIVILEGES;
EOF
if [ $? -eq 0 ]; then
echo -e "${GREEN}✓ 数据库初始化完成!${NC}"
echo -e "${GREEN}=========================================${NC}"
echo -e "${GREEN} 安装成功!${NC}"
echo -e "${GREEN} root 密码:${NEW_PASS}${NC}"
echo -e "${GREEN} 数据库:${DB_NAME}${NC}"
echo -e "${GREEN} 用户:${DB_USER}@'%' 密码:${DB_PASS}${NC}"
echo -e "${GREEN} 时区已设置为东八区(+8:00)${NC}"
echo -e "${GREEN}=========================================${NC}"
else
echo -e "${RED}错误:SQL 执行失败,请检查日志。${NC}"
exit 1
fi
# ------ 可选:重启服务使时区设置生效(其实动态设置已生效) ------
# systemctl restart mysqld
echo -e "${BLUE}>>> 安装全部完成,脚本结束。${NC}"

playbook#

- hosts: share01
vars:
dir: /webshare
wpdir: /wp
uid: 888
user: www
tasks:
- name: "安装nfs服务"
package:
name: nfs-utils
state: present
- name: "拷贝配置文件"
template:
src: ./share/exports.j2
dest: /etc/exports
- name: "创建www组"
group:
name: "{{user}}"
gid: "{{uid}}"
- name: "创建www用户"
user:
name: "{{user}}"
uid: "{{uid}}"
group: "{{user}}"
shell: /sbin/nologin
create_home: false
- name: "创建挂载目录"
file:
path: "{{dir}}"
state: directory
owner: "{{user}}"
group: "{{user}}"
- name: "启动nfs服务"
systemd:
name: nfs-server
state: started
enabled: yes
- name: "解压MySQL压缩包"
unarchive:
src: ./share/mysql-8.0.36-1.el9.x86_64.rpm-bundle.tar
dest: /tmp/
remote_src: no
creates: /tmp/mysql-community-server-8.0.36-1.el9.x86_64.rpm
# 只要这个rpm包存在就不会重复解压
- name: "拷贝mysql执行脚本"
copy:
src: /server/sh/install_mysql.sh
dest: /server/sh/
- name: "执行mysql安装脚本"
shell: "bash /server/sh/install_mysql.sh"
register: install_result
ignore_errors: yes
- name: "如果脚本执行失败,打印错误并中止"
fail:
# fail 模块:主动触发失败,并输出自定义消息
msg: "❌ MySQL 安装失败,错误信息:{{ install_result.stderr_lines }}"
when: install_result.rc != 0
# 当脚本执行失败才执行
- name: "脚本执行成功时打印成功信息"
debug:
msg: "✅ MySQL 安装成功!{{ install_result.stderr_lines }}"
when: install_result.rc == 0
- hosts: web01,web02
vars:
dir: /webshare
tasks:
- name: "web服务安装nfs工具包"
package:
name: nfs-utils
state: present
- name: "web服务挂载共享目录"
mount:
src: 10.2.3.80:/{{dir}}
path: /usr/share/nginx/html/img
fstype: nfs
state: mounted

Wordpress部署#

基础配置#

Terminal window
(.venv) [root@ops-admin Projects]# vim ./nginx/wp.conf
server {
listen 80;
server_name www.wp.com;
root /code/wordpress;
location / {
index index.php index.html;
}
location ~ \.php$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}

Redis缓存#

Terminal window
(.venv) [root@ops-admin Projects]# vim ./share/valkey.conf
# 拷贝至 /etc/valkey/valkey.conf
bind 127.0.0.1 10.2.3.80
protected-mode yes
port 6379
tcp-backlog 511
unixsocket /run/valkey/valkey.sock
timeout 0
tcp-keepalive 300
daemonize no
pidfile /run/valkey/valkey.pid
loglevel notice
logfile /var/log/valkey/valkey.log
databases 16
always-show-logo no
hide-user-data-from-log yes
set-proc-title yes
proc-title-template "{title} {listen-addr} {server-mode}"
locale-collate ""
stop-writes-on-bgsave-error yes
rdbcompression yes
rdbchecksum yes
dbfilename dump.rdb
rdb-del-sync-files no
dir ./
replica-serve-stale-data yes
replica-read-only yes
repl-diskless-sync yes
repl-diskless-sync-delay 5
repl-diskless-sync-max-replicas 0
repl-diskless-load disabled
dual-channel-replication-enabled no
repl-disable-tcp-nodelay no
replica-priority 100
acllog-max-len 128
requirepass 123456
lazyfree-lazy-eviction yes
lazyfree-lazy-expire yes
lazyfree-lazy-server-del yes
replica-lazy-flush yes
lazyfree-lazy-user-del yes
lazyfree-lazy-user-flush yes
oom-score-adj no
oom-score-adj-values 0 200 800
disable-thp yes
appendonly no
appendfilename "appendonly.aof"
appenddirname "appendonlydir"
appendfsync everysec
no-appendfsync-on-rewrite no
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
aof-load-truncated yes
aof-use-rdb-preamble yes
aof-timestamp-enabled no
slowlog-log-slower-than 10000
slowlog-max-len 128
latency-monitor-threshold 0
notify-keyspace-events ""
hash-max-listpack-entries 512
hash-max-listpack-value 64
list-max-listpack-size -2
list-compress-depth 0
set-max-intset-entries 512
set-max-listpack-entries 128
set-max-listpack-value 64
zset-max-listpack-entries 128
zset-max-listpack-value 64
hll-sparse-max-bytes 3000
stream-node-max-bytes 4096
stream-node-max-entries 100
activerehashing yes
client-output-buffer-limit normal 0 0 0
client-output-buffer-limit replica 256mb 64mb 60
client-output-buffer-limit pubsub 32mb 8mb 60
hz 10
dynamic-hz yes
aof-rewrite-incremental-fsync yes
rdb-save-incremental-fsync yes
jemalloc-bg-thread yes

Terminal window
(.venv) [root@ops-admin Projects]# vim ./share/wp-config.php
# 拷贝至 /wp/wordpress/wp-config.php
<?php
define( 'DB_NAME', 'wp' );
define( 'DB_USER', 'wpuser' );
define( 'DB_PASSWORD', 'passwd' );
define( 'DB_HOST', '10.2.3.80' );
define( 'DB_CHARSET', 'utf8mb4' );
define( 'DB_COLLATE', '' );
define( 'AUTH_KEY', 'YYpir)>G!LZ53VG&{hG8j7$M+`Vc`a0S;uj*o)Cz$2vp>AZI-mt_c4`oVvZw:f7P' );
define( 'SECURE_AUTH_KEY', '.Z$ntLw%}<7Cb9h2zA_].4k%ME6#Mkl^b@&F%s?ke<.Cnwt>WvEGJhL0:vl@e}m ' );
define( 'LOGGED_IN_KEY', '8l=Atror^mZ>JkyzS`GlrR{muk8aZ=Jwy2<*s*|p@$DKBcxW{4S7vb@s*EbwP`bN' );
define( 'NONCE_KEY', 'pqS&i&3-r#PhgGS{ {7/Sg}+Jk%{G9Zt+4NsD80&n!a.8%Cr_,o^Ex2xRbTy-zsa' );
define( 'AUTH_SALT', '+SluVE?F;||2f0&a&`;-b=K{}_(5h6Gx(~=&S8&R_+mr[b/H](8*5ES&r(Yb[<(G' );
define( 'SECURE_AUTH_SALT', 'O`SY9|ks|,}SSk3#7m`I6HYav,(rNQDVXt#ETaduD#A@V,Y&EsAN-x-vf{wGUdX>' );
define( 'LOGGED_IN_SALT', '$1VZ-eT5d4;:h#NppV]ROT:$>j]W Dbgx{#MxrV[}.E|@g#Sj=$;:!V#^~=Vs= }' );
define( 'NONCE_SALT', 'qc.s=BnM}uFC{Ws{JKkl@=0A+1*,RKuDw*+X7`;11u?3eT.a?X}N!&Oi5e$C6U)m' );
$table_prefix = 'wp_';
define( 'WP_DEBUG', false );
/* Add any custom values between this line and the "stop editing" line. */
define('WP_REDIS_HOST', '10.2.3.80');
define('WP_REDIS_PORT', 6379);
define('WP_REDIS_PASSWORD', '123456');
/* That's all, stop editing! Happy publishing. */
if ( ! defined( 'ABSPATH' ) ) {
define( 'ABSPATH', __DIR__ . '/' );
}
require_once ABSPATH . 'wp-settings.php';

playbook#

Terminal window
(.venv) [root@ops-admin Projects]# vim playbook-wp.yml
- hosts: share01
vars:
wpdir: /wp
dir: /webshare
user: www
uid: 888
tasks:
- name: "拷贝配置文件"
template:
src: ./share/exports.j2
dest: /etc/exports
- name: "创建挂载目录"
file:
path: "{{wpdir}}"
state: directory
owner: "{{user}}"
group: "{{user}}"
- name: "重启nfs服务"
systemd:
name: nfs-server
state: restarted
- name: "下载wordpress包"
get_url:
url: https://cn.wordpress.org/latest-zh_CN.zip
dest: /tmp/
force: no
- name: "解压WordPress"
unarchive:
src: /tmp/wordpress-7.0-zh_CN.zip
remote_src: yes
dest: "{{wpdir}}"
creates: "{{wpdir}}/wordpress"
- name: "递归修改wordpress目录下的属主&属组"
file:
path: "{{wpdir}}/wordpress"
owner: www
group: www
recurse: yes
- name: "下载valkey缓存"
package:
name: valkey
state: present
- name: "拷贝配置文件 varkey.conf & wp-config.php"
copy:
src: "{{item.src}}"
dest: "{{item.dest}}"
loop:
- src: ./share/valkey.conf
dest: /etc/valkey/valkey.conf
- src: ./share/wp-config.php
dest: /wp/wordpress/wp-config.php
- name: "启动 & 开机自启"
systemd:
name: valkey
state: started
enabled: yes
- hosts: web01,web02
vars:
wpdir: /wp
tasks:
- name: "下载php-redis插件"
package:
name: php-redis
state: present
- name: "拷贝配置文件"
copy:
src: ./nginx/wp.conf
dest: /etc/nginx/conf.d/
- name: "创建code目录"
file:
path: /code/
state: directory
owner: www
group: www
- name: "web服务挂载共享目录"
mount:
src: "10.2.3.80:{{wpdir}}"
path: /code
fstype: nfs
state: mounted
- name: "重启nginx & php-fpm服务"
systemd:
name: "{{item.name}}"
state: restarted
loop:
- name: nginx
- name: php-fpm
- hosts: lb01
tasks:
- name: "修改lb.conf配置文件"
lineinfile:
path: /etc/nginx/conf.d/lb.conf
regexp: "{{item.regexp}}"
line: "{{item.line}}"
loop:
- regexp: "server.*71:8000;"
line: " server 10.2.2.71:80;"
- regexp: "server.*72:8000;"
line: " server 10.2.2.72:80;"
- name: "重启nginx服务"
systemd:
name: nginx
state: restarted
Note

数据库root用户:passwd

  • WordPress数据库:wp
  • 用户:wpuser
  • 密码:passwd
  • 默认的表前缀

  • 管理员用户:admin
  • 密码:Oldboy123.com
Tip

WordPress 博客安装 redis 插件,并启用

Waf雷池#

在waf01节点部署雷池waf

图形化安装#

Terminal window
'最小化节点没有浏览器,所以需要安装图形化'
# 雷池的应用证书,应该在雷池节点导入
yum -y groupinstall 'Server with GUI'
# 安装图形化
systemctl set-default graphical.target
# 设置默认进入图形界面
systemctl isolate graphical.target
# 切换到图形界面

安装脚本#

#!/bin/bash
set -e # 遇到错误立即退出
# ===== 1. 安装 iptables-nft(如果未安装) =====
if ! rpm -q iptables-nft &>/dev/null; then
echo "正在安装 iptables-nft..."
dnf install -y iptables-nft
else
echo "iptables-nft 已安装,跳过。"
fi
# ===== 2. 安装 Docker(如果未安装) =====
if ! command -v docker &>/dev/null; then
echo "Docker 未安装,开始安装..."
curl -sSL kpyun.fun/scripts/install-docker.sh | bash -s i
else
echo "Docker 已安装,跳过。"
fi
# ===== 新增:创建 Docker Compose 插件软链接(仅当不存在时) =====
mkdir -p /usr/libexec/docker/cli-plugins/
# 先判断软链接是否存在,已存在则跳过
if [ ! -L /usr/libexec/docker/cli-plugins/docker-compose ]; then
# 软链接不存在,再判断源文件是否存在,避免无效链接
if [ -f /usr/local/docker/docker-compose ]; then
# 源文件存在,且软链接不存在则,创建软链接
ln -s /usr/local/docker/docker-compose /usr/libexec/docker/cli-plugins/docker-compose
echo "软链接已创建:/usr/libexec/docker/cli-plugins/docker-compose -> /usr/local/docker/docker-compose"
else
echo "警告:源文件 /usr/local/docker/docker-compose 不存在,跳过软链接创建。"
fi
else
echo "软链接已存在,跳过。"
fi
# ===== 3. 准备 WAF 目录和 compose 文件 =====
mkdir -p /wp && cd /wp
# compose.yaml 每次都重新下载(文件小无需判断)
echo "下载 compose.yaml..."
wget -q -O compose.yaml "https://waf-ce.chaitin.cn/release/latest/compose.yaml"
# ===== 4. 生成 .env 环境变量文件(幂等:内容不变就不重复写) =====
# 用临时文件对比,避免每次都触发 docker-compose 重新创建
cat > .env.tmp <<'EOF'
SAFELINE_DIR=/wp
IMAGE_TAG=9.1.0-lts
MGT_PORT=9443
POSTGRES_PASSWORD=Redhat123
SUBNET_PREFIX=172.22.222
IMAGE_PREFIX=chaitin
ARCH_SUFFIX=
RELEASE=
REGION=
MGT_PROXY=0
EOF
if ! cmp -s .env .env.tmp 2>/dev/null; then
mv .env.tmp .env
echo ".env 文件已更新。"
else
rm -f .env.tmp
echo ".env 文件无变化,跳过。"
fi
# ===== 5. 导入 Docker 镜像(仅当镜像包存在时) =====
if [ -f /tmp/all-9.1.0-lts.tar ]; then
echo "导入 Docker 镜像..."
docker image load < /tmp/all-9.1.0-lts.tar
rm -f /tmp/all-9.1.0-lts.tar
else
echo "未找到镜像包 /tmp/all-9.1.0-lts.tar,跳过导入。"
fi
# ===== 6. 启动容器(docker-compose up -d 本身幂等) =====
docker-compose up -d
# ===== 7. 生成自签名证书(如果不存在) =====
mkdir -p ./ssl
if [ ! -f ./ssl/www.wp.com.key ] || [ ! -f ./ssl/www.wp.com.crt ]; then
echo "生成自签名证书..."
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout ./ssl/www.wp.com.key \
-out ./ssl/www.wp.com.crt \
-subj "/C=CN/ST=Beijing/L=Beijing/O=Group2/CN=www.wp.com"
else
echo "证书已存在,跳过生成。"
fi
echo "✅ 所有步骤完成。"

playbook#

- hosts: waf01,lb01,web01,web02
tasks:
- name: "添加 hosts 解析记录"
lineinfile:
path: /etc/hosts
line: "10.2.2.50 www.wp.com"
- hosts: waf01
vars:
waf_image: "all-9.1.0-lts.tar"
remote_image_path: "/tmp/{{ waf_image }}"
compose_dir: "/wp"
tasks:
# 1. 安装 iptables-nft(yum/dnf 模块天然幂等)
- name: "安装 iptables-nft 兼容包"
dnf:
name: iptables-nft
state: present
# 2. 安装 Docker(如果未安装则执行安装脚本,利用 creates 实现幂等)
- name: "安装 Docker(若尚未安装)"
shell: "curl -sSL kpyun.fun/scripts/install-docker.sh | bash -s i"
args:
creates: "/usr/bin/docker" # 若 docker 二进制已存在,跳过执行
# ***** 新增:创建 docker compose 插件软链接(仅当不存在时) *****
- name: "创建 docker compose 插件软链接"
shell: "ln -s /usr/local/docker/docker-compose /usr/libexec/docker/cli-plugins/docker-compose"
args:
creates: "/usr/libexec/docker/cli-plugins/docker-compose" # 若目标链接已存在,跳过
# ***** 新增:检查 safeline-mgt 镜像是否存在(用于后续条件判断) *****
- name: "检查 safeline-mgt 镜像是否存在"
shell: "docker image inspect chaitin/safeline-mgt:9.1.0-lts >/dev/null 2>&1"
register: image_check
failed_when: false # 强制任务永远成功,不会标记为 failed
changed_when: false # 该任务不会改变系统状态,抑制changed
# 3. 拷贝镜像包(copy 模块幂等:内容不变则不重复拷贝)
- name: "拷贝雷池镜像包到远程"
copy:
src: "./waf/{{ waf_image }}"
dest: "{{ remote_image_path }}"
# 4. 创建工作目录并下载 compose.yaml
- name: "创建 /wp 目录"
file:
path: "{{ compose_dir }}"
state: directory
- name: "下载最新 compose.yaml"
get_url:
url: "https://waf-ce.chaitin.cn/release/latest/compose.yaml"
dest: "{{ compose_dir }}/compose.yaml"
force: no
# 如果存在,则不进行下载,不强制更新
# 5. 生成 .env 文件(template 模块可控制幂等,但这里用 copy 提供固定内容)
- name: "生成 .env 环境变量文件"
copy:
content: |
SAFELINE_DIR=/wp
IMAGE_TAG=9.1.0-lts
MGT_PORT=9443
POSTGRES_PASSWORD=Redhat123
SUBNET_PREFIX=172.22.222
IMAGE_PREFIX=chaitin
ARCH_SUFFIX=
RELEASE=
REGION=
MGT_PROXY=0
dest: "{{ compose_dir }}/.env"
# 如果文件内容不变,则不会触发变更
# 6. 导入 Docker 镜像(仅当镜像包存在时执行)
- name: "导入 Docker 镜像"
shell: "docker image load < {{ remote_image_path }}"
args:
removes: "{{ remote_image_path }}" # 只有文件存在时才执行,不存在则跳过
# 执行后删除镜像包(可选)
notify: "清理镜像包"
# ***** 修改:7. 启动容器,仅在镜像不存在时执行 *****
- name: "启动雷池容器(若镜像不存在)"
community.docker.docker_compose_v2:
project_src: "{{ compose_dir }}"
state: present
when: image_check.rc != 0 # 只有当镜像不存在时才启动
# 8. 生成自签名证书
- name: "确保 ssl 目录存在"
file:
path: "{{ compose_dir }}/ssl"
state: directory
- name: "生成自签名证书"
shell: |
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout /wp/ssl/www.wp.com.key \
-out /wp/ssl/www.wp.com.crt \
-subj "/C=CN/ST=Beijing/L=Beijing/O=Group2/CN=www.wp.com"
args:
creates: "/wp/ssl/www.wp.com.crt" # 证书已存在则跳过,保证幂等
handlers:
- name: "清理镜像包"
file:
path: "{{ remote_image_path }}"
state: absent
Note

creates 是 Ansible 为 shell/command 这类天然非幂等模块提供的最简幂等保障机制,用文件存在性作为”任务是否已完成”的判断依据

⚠️ 注意creates 只检查文件/目录是否存在,不检查内容、版本或权限。如果 /usr/bin/docker 已存在但版本不对,任务仍会被跳过。

参数逻辑场景
creates文件存在则跳过安装/创建资源
removes文件不存在则跳过删除/清理资源
Terminal window
[root@waf01 wp]# docker exec safeline-mgt resetadmin
# 每次执行 resetadmin 会生成新密码
[INFO] Initial username:admin
[INFO] Initial password:5zb7qCqS
[INFO] Done
# 在打开页面之前生成一次密码,并在屏幕中捕获它~
# https://10.2.2.50:9443
Terminal window
[root@waf01 wp]# mkdir -p /usr/libexec/docker/cli-plugins/
[root@waf01 wp]# ln -s /usr/local/docker/docker-compose /usr/libexec/docker/cli-plugins/docker-compose
[root@waf01 wp]# docker compose version
Docker Compose version v2.29.2

Jumpserver#

Terminal window
[root@jumpserver ~]# dnf install -y iptables-nft
[root@jumpserver ~]# ls /tmp/
jumpserver-ce-v4.10.16-x86_64.tar.gz
[root@jumpserver ~]# tar xf /tmp/jumpserver-ce-v4.10.16-x86_64.tar.gz -C /opt/
[root@jumpserver ~]# cd /opt/jumpserver-ce-v4.10.16-x86_64/
[root@jumpserver jumpserver-ce-v4.10.16-x86_64]# ./jmsctl.sh install
>>> The Installation is Complete
1. You can use the following command to start, and then visit
cd /opt/jumpserver-ce-v4.10.16-x86_64
./jmsctl.sh start
2. Other management commands
./jmsctl.sh stop
./jmsctl.sh restart
./jmsctl.sh backup
./jmsctl.sh upgrade
For more commands, you can enter ./jmsctl.sh --help to understand
3. Web access
http://10.0.0.40:80
Default username: admin Default password: ChangeMe
More information
Official Website: https://www.jumpserver.com/
Documentation: https://www.jumpserver.com/docs
curl -sSL https://resource.fit2cloud.com/jumpserver/jumpserver/releases/latest/download/quick_start.sh | bash

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

Ansible角色
https://www.jzops.fun/posts/automation/ansible/ansible05/
作者
久棹
发布于
2026-06-10
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
久棹
只要胆子大,天天寒暑假!
公告
欢迎来到久棹的技术小站!本站专注 Linux 运维学习笔记分享,如有问题欢迎交流探讨 🎉
分类
标签
站点统计
文章
98
分类
11
标签
203
总字数
240,073
运行时长
0
最后活动
0 天前
站点信息
构建平台
Local
博客版本
Firefly v6.13.5
文章许可
CC BY-NC-SA 4.0

文章目录