本节摘要:一节全程实战:从空目录开始开发一个生产级的"应用运行时"角色,经过骨架生成、参数设计、模板编写、本地测试、版本化,最后发布供其他项目复用。本节把 4.1 的结构知识与 4.2 的分发知识走完一个完整生命周期,途中反复用到 4.4 的调试手段。
选定"app_runtime"作为实战角色:负责把一个 Java 或 Python 应用跑起来——创建运行用户、准备目录、渲染配置、安装服务单元、启动并验证。选它的理由是通用性:几乎所有服务型应用都需要这组动作,写好一个就能在任何项目复用。命名遵循正式规则:命名空间可选,角色名一律小写加下划线。
手工建目录也行,但官方脚手架一条命令生成全部约定结构,还附带示例文件可参考:
ansible-galaxy init roles/app_runtime tree roles/app_runtime # defaults files handlers meta tasks templates tests vars # 每个目录下都有 main.yml 占位
删掉用不到的目录(tests 占位目录换成真正的 molecule 测试,见后文),README.md 第一时间写参数表——先写文档再写代码,参数设计想不清楚就说明角色边界还没想清楚。
先把参数表列出来,再动手写任务。设计原则:defaults 放"不覆盖也正确"的值,必填参数不设默认值并在任务里用 mandatory 防呆。
# defaults/main.yml app_name: "" # 必填:应用名(服务名、用户名、目录名都从它派生) app_user: "{{ app_name }}" app_group: "{{ app_name }}" app_version: "{{ app_version_override | mandatory }}" # 必填:发布版本 app_port: 8080 app_base_dir: "/opt/{{ app_name }}" app_config: {} # 应用配置字典:交给模板逐项展开 app_java_opts: "-Xms512m -Xmx512m"
设计上有两个决策值得说明。其一,app_name 是"派生锚点":用户、目录、服务名都从它派生,调用方只需给一个名字,这是减少参数数量的关键手法。其二,app_config 用开放字典而不是逐个参数:应用配置项千差万别,角色不该为每个配置键建变量——角色管"配置文件存在且由模板渲染",配置内容本身是调用方的领域。这个边界划分让角色在十年内不需要为应用配置变更而改版。
任务入口按能力域分段:
# tasks/main.yml --- - name: 创建运行用户与组 ansible.builtin.group: name: "{{ app_group }}" state: present - name: 创建运行用户 ansible.builtin.user: name: "{{ app_user }}" group: "{{ app_group }}" shell: /sbin/nologin create_home: false - name: 准备目录结构 ansible.builtin.file: path: "{{ item }}" state: directory owner: "{{ app_user }}" group: "{{ app_group }}" mode: "0755" loop: - "{{ app_base_dir }}" - "{{ app_base_dir }}/conf" - "{{ app_base_dir }}/logs" - name: 渲染应用配置 ansible.builtin.template: src: application.conf.j2 dest: "{{ app_base_dir }}/conf/application.conf" owner: "{{ app_user }}" mode: "0640" notify: 重启应用 - name: 安装服务单元 ansible.builtin.template: src: app.service.j2 dest: "/etc/systemd/system/{{ app_name }}.service" mode: "0644" notify: 重载 systemd - name: 确保服务启动并自启 ansible.builtin.systemd: name: "{{ app_name }}" state: started enabled: true
服务单元模板是角色的点睛之笔——硬编码的服务文件一改就丢,模板化的服务文件永远与角色同步:
# templates/app.service.j2 [Unit] Description={{ app_name }} service After=network.target [Service] Type=simple User={{ app_user }} WorkingDirectory={{ app_base_dir }} Environment=JAVA_OPTS={{ app_java_opts }} ExecStart={{ app_base_dir }}/bin/run.sh Restart=on-failure RestartSec=5 [Install] WantedBy=multi-user.target
角色开发的最小验证环:一个测试剧本加一台虚拟机(或容器)。
# test.yml --- - hosts: testvm roles: - role: app_runtime vars: app_name: demo app_version_override: "1.0.0" app_config: log_level: debug
ansible-lint test.yml roles/app_runtime # 静态检查:命名、幂等写法、弃用语法 ansible-playbook test.yml --check --diff -i test.ini # 干跑看将要发生的变更 ansible-playbook test.yml -i test.ini # 实跑 ansible-playbook test.yml -i test.ini # 二次跑必须 changed=0
lint 的输出值得逐条消化:它检查的是社区沉淀的写法纪律(每个任务有 name、包操作用模块而非 shell、yamllint 规范),新角色跑绿 lint 是发布的前置条件。二次跑 changed=0 则是 3.6 节幂等自检的直接应用。更进一步的自动化测试用 molecule 框架:它把"起一个干净容器、跑角色、断言结果、销毁"整环自动化,是角色持续集成的事实标准——第 5 章把它放进工具链版图。
角色测试通过后进入发布环节。版本语义遵循常规三段式:破坏参数兼容升主版本,新增参数升次版本,修 bug 升修订号。发布前的检查单:README 参数表与 defaults 逐项核对;meta/main.yml 的 min_ansible_version 与依赖已声明;CHANGELOG 记录本版变更;全量 lint 通过。发布方式按 4.2 节的路线二选一:git 打标签(内部消费),或推送到索引服务(跨团队分发)。
git tag -a v1.0.0 -m "首个稳定版:用户目录配置服务五段式" git push origin v1.0.0
消费方在自己的 requirements 里锁定这个标签:
roles: - name: app_runtime src: git+内部仓库地址/ansible-roles/app_runtime.git version: v1.0.0
发布不是终点。三类维护信号要有响应机制:消费方提的参数需求(评估是通用需求还是特例——特例进调用方,通用需求进角色并升次版本);lint 与依赖的弃用告警(排期处理,别等硬报错);幂等性退化报告(任何"升级后每次跑都 changed"的反馈都是高优缺陷)。维护纪律的底线:角色的每一次变更都过测试环,宁慢勿坏——角色是复用单元,一个坏版本放大会波及所有消费方。
复盘这一节的生命周期:骨架、参数、任务、验证、发布、维护,六个环节里真正难的只有参数设计——它决定角色能活多久。回到第 4 章开头的那句话:复用结构与保持简单之间偏向简单。下一章跳出项目内部,看 Ansible 在更大工具版图里的位置。