# Ansible and Spring application Deployment **Repository Path**: jeffwang78/ansible-spring-application ## Basic Information - **Project Name**: Ansible and Spring application Deployment - **Description**: 教程:使用 ansible 部署 spring 应用 - **Primary Language**: Java - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 1 - **Created**: 2022-11-10 - **Last Updated**: 2024-07-15 ## Categories & Tags **Categories**: Uncategorized **Tags**: Ansible, SpringBoot, jinja2 ## README # Ansible and Spring application Deployment ## 介绍 使用 ansible 部署 spring 应用的简单示例。 ## 使用说明 本文演示的Ansible 代码,应在 linux 上运行,由于内部使用了 apt 安装包,建议在 ubuntu 上使用。 ## 前言 Ansible 是强大的管理工具,它基于 SSH 实现对目标机器的管理。如果仅仅如此,那么,它与SHELL 脚本系统有什么差别和优势呢? 答案是,Ansible 提供了: - 丰富的、成熟可用的模块帮助开发者完成绝大部分任务。 - 优秀的模板技术实现配置自动化。 - 模块的原子操作设计理念,确保任务执行的可重复性。 - 多样化的管理逻辑。 - 良好的代码组织结构。 ## Part 1:准备 ### 1.1 安装 Python Ansible 基于Python开发,因此,需要在控制机和目标机均安装Python环境。这在 Linux 环境下不是什么特殊需求。大部分Linux 系统自带Python2.7,以可满足Ansible绝大部分功能需求。 虽然 Python 2 也可以使用,但仍然推荐使用Python3。 如需安装Python3, 可使用 APT 或 YUM 进行安装。本文以 APT 为例。 ```` bash sudo apt install python3 ... python --version Python 2.7.17 ```` *注意:* > python3 默认名称是 python3。因此,需要修改`/usr/bin/python` 将其链接到 python3。 ```` bash cd /usr/bin/ sudo mv python python2 sudo ln -s python3 python python --version Python 3.6.9 ```` ### 1.2. 安装 SSH Ansible 需要 ssh 连接并管理目标机,因此,需要在管理机和目标机均安装ssh。通常Linux已经安装好了。 如未安装,则使用apt或yum安装。 ````bash sudo apt install openssh-server # 启动服务 sudo systemctl start ssh # 设定开机自启动 sudo systemctl enable ssh ```` *注意:* > 如系统未安装 systemctl 则可使用 service 进行服务管理 > 如目标机需要使用root登录,需修改ssh配置文件: ````bash vi /etc/ssh/sshd_config permitRootLogin yes permitRootLogin prohibit-password ```` ### 1.3 安装 Ansible 最简单的安装方法仍然是使用 APT。Ansible 官方推荐使用 pip 安装,但过程更为复杂。推荐使用 APT 安装。 ````bash sudo apt install ansible ansible --version ansible [core 2.11.12] config file = /etc/ansible/ansible.cfg configured module search path = ['~/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules'] ansible python module location = ~/.local/lib/python3.6/site-packages/ansible ansible collection location = ~/.ansible/collections:/usr/share/ansible/collections executable location = ~/.local/bin/ansible python version = 3.6.9 (default, Jun 29 2022, 11:45:57) [GCC 8.4.0] jinja version = 3.0.3 libyaml = True ```` *注意:* > 如 `python version = 2.7`, 这是因为没有替换python3 的原因。参照上文重新建立软连接即可。 ## Part 2: 基础 ### 2.1 Ping Ansible 提供命令行工具,可以执行简单的ansible模块命令。使用它开始: ````bash ansible localhost -m ping localhost | SUCCESS => { "changed": false, "ping": "pong" } ```` 这行命令中, `localhost` 代表目标机,这里使用了 "**本机**"这个ansible内置的名字。`-m` 代表执行一个模块命令,`ping`是命令的名字。 `ping` 表示连接目标机并确认其可达(reachable)。命令结果显示 `SUCCESS`。 ### 2.2 Hello world! 如果仅仅使用命令一个一个的执行,那么Ansible也就没有任何吸引力了。 Ansible可以将一系列命令组合起来,形成一个**Playbook**,你可以将其与 Shell Script 联系起来加强其理解。 Playbook 采用YAML格式编写。按照惯例,编写一个简单的 helloworld.yml (在本项目的`/deploy/`目录下可以找到)。 ````yaml - hosts: localhost # 定义目标机,本例使用 localhost gather_facts: no tasks: # 需要在目标机执行的任务列表 - name: hellow world # 第一个任务的名称 debug: # 调用 debug 模块 msg: Hello world! # 模块参数:msg ```` *注意:* > 设置`gather_facts: no` 是为了加快执行速度。 > Ansible中的 boolean 类型支持 yes/no 或 True/False。 执行 Playbook,需要使用 ansible-playbook 命令: ````bash ansible-playbook deploy/helloworld.yml PLAY [localhost] ************************************************************************************ TASK [hello world] ************************************************************************************ ok: [localhost] => { "msg": "Hello world!" } PLAY RECAP ************************************************************************************ localhost : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 ```` 执行结果中,可以看到: - `PLAY [localhost]` 表示执行的目标机。 - `TASK [hello world]`表示执行了该任务。 - `ok: [localhost] => ...` 表示执行成功。 - `PLAY RECAP` 中详细描述了各个目标机的执行情况。 ### 2.3 Hello again 为helloworld增加一个熟悉的 ping 命令。 ````bash ... - name: ping ansible.builtin.ping: ```` *注意:* > 与之前的 ping 命令相比,ping 前面多了 `ansible.builtin.`,这是 ansible默认的模块名空间(namespace),可以不写。 执行后,可以看到输出内容增加了: ````bash TASK [ping] *********************************************************************************** ok: [localhost] PLAY RECAP ********************************************************************************** localhost : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 ```` ### 2.4 部署Spring应用 下面将进行第一次Spring应用的部署。部署前需要确保Java8 和 maven已经安装好。同样可以使用 APT 进行安装。 #### 2.4.1 部署 spring 应用基本步骤 1. 使用 mvn打包。 2. 将jar复制到目标机安装目录。 3. 启动应用。 #### 2.4.2 编制第一个playbook : first.yml 使用最简单的模块shell执行部署(参见/deploy/first.yml): ````yml - hosts: localhost gather_facts: no tasks: - name: package spring app ansible.builtin.shell: chdir: ../ansible-spring-example1/ cmd: mvn package -DskipTests=true - name: install spring app ansible.builtin.copy: src: ../ansible-spring-example1/target/ansible-spring-example1-0.0.1.jar dest: /var/lib/as-example1/ - name: start spring app ansible.builtin.shell: chdir: /var/lib/as-example1/ cmd: source /etc/profile ; java -jar ansible-spring-example1-0.0.1.jar & ```` - ansible.builtin.shell `ansible.bulitin.shell`模块用来执行任意脚本命令。其两个主要参数分别是: * `chdir: ../ansible-spring-example1/` 命令执行的目录 * `cmd: mvn package -DskipTests=true` 需要执行的命令 上述参数类似与在目标机依次执行: ````bash cd ../ansible-spring-example1/ mvn package -DskipTests=true ```` *注意:* > `source /etc/profile ; `的目的是,取得 $JAVA_HOME环境变量设置。由于Ansible 采用 nologin 方式登录目标机,因此不会执行 /etc/profile。这样,java命令可能会失败。 >> 其他需要环境变量设置的命令,也需要考虑增加该命令。或,使用 enviorment 定义环境变量。 - ansible.builtin.copy `ansible.bulitin.copy`模块用来将文件或目录复制到目标机。其两个主要参数分别是: * `src: ../ansible-spring-example1/target/ansible-spring-example1-0.0.1.jar` 待复制的源文件 * `dest: /var/lib/as-example1/` (目标机上)目的文件、目录。注意,目录需要结尾处的 `/`。 `copy`并非简单的进行复制,而会对比源、目的文件是否一致,如一致则不会再进行复制(对应结果 `changed=false`)。对比文件的方式是采用文件hash。 *注意:* > 本例中,/var/lib目录可能需要root权限才能访问,因此,如出现`permission denied`,可使用sudo 执行playbook。 #### 2.4.3 执行first.yml 使用 playbook 执行 : ````bash ansible-playbook deploy/first.yml ```` mvn 打包过程可能会比较慢。执行后,只需要检查应用是否启动即可: ````bash ps -ef | grep ansible-spring-example1-0.0.1.jar root 26845 22565 0 Nov10 ? 00:02:41 java -jar ansible-spring-example1-0.0.1.jar ```` 至此,第一个版本的部署playbook已经完成。 第一个版本处理流程很简单,下面对其进行改进。 #### 2.4.4 使用变量Varible first.yml 中有多次重复出现的内容,如:`/var/lib/`, `ansible-spring-example1`。假如需要修改安装路径,那么如何方便的进行修改呢? 使用变量(Varible)可以轻松做到这一点。 将上述内容定义为变量,并在整个脚本中引用它们,如下: ````yaml - hosts: localhost gather_facts: no vars: path: ../ansible-spring-example1/ name: ansible-spring-example1 version: 0.0.1 install_path: /tmp ```` 在 palybook 中使用 vars 来定义一个字典(dict), 之后,可以在后面的task中引用。 ````yaml - name: install spring app - {{ name }} version {{ version }} ansible.builtin.copy: src: "{{ path }}/target/{{ name }}-{{ version }}.jar" dest: "{{ install_path }}/{{ name }}/" ```` Ansible引用一个变量的方式是,使用 `{{ }}` 将其包围起来。 `{{}}`可视为一个变量模板,模板中可以进行各类计算。上述 `src:`的内容,可替换成的等效语句: `{{ path + '/target/' + name + '-' + version + '.jar' }}` > *Ansible 使用Jinja2 模板语言来支持更复杂强大的操作。* 有意思的是,name 中也是可以使用变量的。*如果这个任务将被其他任务或handler引用,那么不建议这样做。* 进一步探究变量,变量可以理解为一个简单对象(可以把它当作 JSON 对象 来理解)。因此,变量可以很复杂,如: ````yaml vars: app_meta: path: ../ansible-spring-example1/ name: ansible-spring-example1 version: 0.0.1 install_path: /tmp jvm_args: - '-Xms128m' - '-Xmx256m' ```` 将spring 应用的变量整合为一个变量: app_meta。 *注意:* > app_meta 还包括了一个 list 类型的 jvm_args属性。该属性使用将结合模板进行介绍。 而在task中引用这些变量形式为: ````yaml - name: install spring app - {{ app_meta.name }} version {{ app_meta.version }} ansible.builtin.copy: src: "{{ app_meta.path }}/target/{{ app_meta.name }}-{{ app_meta.version }}.jar" dest: "{{ app_meta.install_path }}/{{ app_meta.name }}/" ```` *注意:* > `src: "{{ app_meta.path }}"`,当一行的开始即引用了变量,则需要在其上添加引号。这是为了和YAML语法兼容。并非说明这是一段字符串。 以上改进后的playbook 在/depoly/first2.yml 中。同样使用`ansible-playbook` 运行。 ### 2.5 使用模板Template first2.yml 中,使用了 java 命令启动 spring 应用,这是既简单的形式,在实际应用中,需要使用更为复杂的一系列命令来完成,通常会使用shell脚本来完成。 #### 2.5.1 使用脚本 因此,可以使用脚本来完成: startapp.sh ````bash #!/bin/bash source /etc/profile nohup java -jar ansible-spring-example1-0.0.1.jar >stdout.log 2>&1 & echo Start Success! ```` 同样的,制作一个简单的 stopapp.sh : ````bash #!/bin/sh source /etc/profile tpid=`ps -ef|grep ansible-spring-example1-0.0.1.jar |grep -v grep|grep -v kill|awk '{print $2}'` if [ ${tpid} ]; then echo 'Kill Process!' kill -9 $tpid fi ```` *注意:* > 上述脚本内容过于简单,并不能适用于真实应用。可以使用其他适宜的脚本替代。 当部署Spring APP时,需要将该文件复制到install_path下。 在first3.yml中增加copy的 task。可以直接增加: ````yaml - name: copy startapp.sh ansible.builtin.copy: src: "startapp.sh" dest: "{{ app_meta.install_path }}/{{ app_meta.name }}/" - name: copy stopapp.sh ansible.builtin.copy: src: "stopapp.sh" dest: "{{ app_meta.install_path }}/{{ app_meta.name }}/" ```` 另一种做法是,使用循环。Ansible支持task循环,本次使用遍历list的循环: ````yaml - name: install spring app - {{ app_meta.name }} version {{ app_meta.version }} ansible.builtin.copy: src: "{{ item }}" dest: "{{ app_meta.install_path }}/{{ app_meta.name }}/" with_list: - "{{ app_meta.path }}/target/{{ app_meta.name }}-{{ app_meta.version }}.jar" - startapp.sh - stopapp.sh ```` with_list表示遍历一个list,在其后定义了一个list,里面包含三个文件: - .jar - startapp.sh - stopapp.sh `copy`的 src 参数,使用了 {{ item }}。**item** 是循环中定义的变量,代表遍历的元素。 任务执行时,可以看到遍历的效果和 item 的取值: ````bash TASK [install spring app - ansible-spring-example1 version 0.0.1] ***************************************** ok: [localhost] => (item=../ansible-spring-example1//target/ansible-spring-example1-0.0.1.jar) ok: [localhost] => (item=startapp.sh) ok: [localhost] => (item=stopapp.sh) ```` 当然,还要修改Spring app启动命令,分别调用 stop/start 脚本,达到重启动的目的。 ````yaml - name: restart spring app - {{ app_meta.name }} version {{ app_meta.version }} ansible.builtin.shell: chdir: "{{ app_meta.install_path }}/{{ app_meta.name }}" cmd: "{{ item }}" with_list : - bash stopapp.sh - bash startapp.sh ```` 同样使用循环方式依次调用脚本。 #### 2.5.2 模板文件 如果start/stop 脚本中的jar文件名字可以替换成变量app_meta.name,那么脚本就更具有通用性了。如何做到这一点呢? 使用模板可以实现文件参数化。Ansible内置Jinja2模板,可以轻松完成这一任务。 - ansible.bulitin.template 为此,建立模板文件templates/startapp.sh.j2,先将startapp.sh内容复制过去。再进行修改。 *注意:* > Ansible会自动在templates目录下寻找模板文件, 因此,将模板组织在templates目录是一种方便的做法。 使用{{ }}形式将应用名称等信息参数化。 **`startapp.sh.j2`** ````bash #!/bin/bash source /etc/profile nohup java -jar {{ app_meta.name }}-{{ app_meta.version }}.jar >stdout.log 2>&1 & echo Start {{ app_meta.name }} version {{ app_meta.version }} Success! ```` 同样修改 templates/stopapp.sh.j2 模板文件准备好之后,使用 template 改写first4.yml ````yaml - name: copy start/stop sh ansible.builtin.template: src: "{{ item.src }}" dest: "{{ app_meta.install_path }}/{{ app_meta.name }}/{{ item.dest }}" backup: yes with_list: - {src: startapp.sh.j2, dest: startapp.sh} - {src: stopapp.sh.j2, dest: stopapp.sh} ```` 大致上,内容与之前的 copy 类似: - copy 变为 template - src 参数 - destc 参数 - with_list 但注意到,with_list 的元素形式为一个对象,包括 src 和 dest 两个属性,这是因为模板文件名后缀.j2在部署后有需要去掉,因此,dest文件名需要单独定义。后文会介绍使用filter来简单这一点小问题。 由于元素结构变化,src引用的内容也就相应变成了 {{ item.src }}。 `backup: yes` 表示当文件有变化时,将源文件进行备份。这是很有价值的,在配置文件变化时,保留一个旧的副本,一旦出现问题时可以进行对比分析。 使用 `ansible-playbook deploy/first4.yml` 来重新部署你的应用。 ## Part 3: 复用 上一版本的playbook已经能完成基本的功能,考虑实际使用的情况,尤其在微服务架构流行的形式下,大量App 需要同时部署,因此,能否编写一套playbook, 对不同的APP使用呢? ### 3.1 include task first4.yml的tasks已经参数化了,通过修改vars中的变量,就可以执行不同APP的部署。在此基础上,借鉴函数的概念,将first4.yml当作一个函数,只需要传入不同的变量,即可完成APP部署Playbook的复用: - 将vars变量提取到外部 - 在其他playbook设置应用变量 - 调用first4.yml #### 3.1.1 提取变量 将first4.yml复制一份,改名为:include/app1.yml。 并将tasks以上部分均删除或注释。如: ````yaml # tasks: --- - name: package spring app - {{ app_meta.name }} version {{ app_meta.version }} ansible.builtin.shell: ```` #### 3.1.2 app_deploy.yml 创建playbook: app_deploy.yml。编写Playbook 基本内容,并将 first4.yml中的vars部分复制其中。 ````yaml - hosts: localhost gather_facts: no vars : app_meta: path: ../ansible-spring-example1/ name: ansible-spring-example1 version: 0.0.1 install_path: /tmp jvm_args: - -Xms128M - -Xmx256M tasks: ```` Ansible 提供了 include_tasks 模块来包含playbook. 在app_deploy.yml 添加该调用即可。 ````yaml tasks: - name: call to app1.yml ansible.builtin.include_tasks: file: include/app1.yml ```` 运行该Playbook: ````bash $ ansible-playbook deploy/app_deploy.yml ... TASK [call to app1.yml] ***************************************************************************************** included: /home/mvnprojects/ansible-spring-application/deploy/include/app1.yml for localhost ... ```` 可见调用已经成功了。 #### 3.1.3 调用时传递变量 app_deploy.yml 在include app1.yml时,没有传递变量,app1.yml 使用的变量 `app_meta` 实际上就是 vars 里面定义的`app_meta`。假如将vars 的 app_meta名字修改一下,那么执行会出错。如: ````yaml vars: app_meta1: ```` 执行后出现: ````bash fatal: [localhost]: FAILED! => {"msg": "The task includes an option with an undefined variable. The error was: 'app_meta' is undefined .... ```` 如何修正呢?所有的ansible模块都可以传递变量,同样使用 vars的形式,如: ````yaml - name: call to app1.yml ansible.builtin.include_tasks: file: include/app1.yml vars: app_meta: "{{ app_meta1 }}" ```` 调用app1.yml时,将 变量 app_meta 赋值为 app_meta1,运行: ````bash PLAY RECAP *********************************************************************************** localhost: ok=5 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 ```` #### 3.1.4 多APP部署 很自然,在include_tasks上添加循环,即可实现多个APP的依次安装。 为此,首先将 app信息定义拓展为 list 。将 app_meta 修改为 app_meta_list,并将原内容作为 list 元素。任意编写 另一个 APP 信息(example2): ````yaml vars : app_meta_list: - name: ansible-spring-example1 path: ../ansible-spring-example1/ version: 0.0.1 install_path: /tmp jvm_args: - -Xms128M - -Xmx256M - name: example2 path: ../example2/ version: 0.0.1 install_path: /tmp jvm_args: - -Xs64M - -Xm128M ```` 在task 上加入with_list 循环: ````yaml - name: call to app1.yml ansible.builtin.include_tasks: file: include/app1.yml vars: app_meta: "{{ item }}" with_list: "{{ app_meta_list }}" ```` 运行结果却出乎意料: ```` TASK [package spring app - ansible-spring-example1 version 0.0.1] *********************************************** changed: [localhost] TASK [install spring app - ansible-spring-example1 version 0.0.1] fatal: [localhost]: FAILED! => {"msg": "The task includes an option with an undefined variable. The error was: 'ansible.utils.unsafe_proxy.AnsibleUnsafeText object' has no attribute 'install_path' ```` 错误原因是变量未定义:**has no attribute 'install_path'** 这是如何产生的呢?回顾app1.yml,其中有一段循环: ````yaml - name: install spring app - {{ app_meta.name }} version {{ app_meta.version }} ansible.builtin.copy: src: "{{ item }}" dest: "{{ app_meta.install_path }}/{{ app_meta.name }}/" with_list: - "{{ app_meta.path }}/target/{{ app_meta.name }}-{{ app_meta.version }}.jar" ```` 其中循环的默认变量是 **item**, 而在app_depoly 中,循环的默认变量也是 **item**, 因此,app1循环的 item 变量 覆盖了 app_deploy 的 Item, 导致 无法查找到 item.install_path。 自然,Ansible 设计时考虑了这一点,因此,提供了对循环变量自定义名称的办法: ````yaml - name: call to app1.yml ansible.builtin.include_tasks: file: include/app1.yml vars: app_meta: "{{ list_item }}" with_list: "{{ app_meta_list }}" loop_control: loop_var: list_item ```` 通过 loop_var,将循环变量默认的item修改为**list_item**,并在vars中引用。 再次运行,结果成功(当然,由于 Example2 不存在,因此该执行会失败,但逻辑是正确的)。 *注意:* > 在出现嵌套循环且使用默认命名item时,ansible 会给出警告:**[WARNING]: The loop variable 'item' is already in use. You should set the `loop_var` value in the `loop_control` option for the task to something else to avoid variable collisions and unexpected behavior.**,如出现,应立即进行loop_var定义。 ### 3.1.5 字典形式变量 (dict) Ansible 允许使用字典形式的变量,这种方式的好处之一书写方便,避免重复。 例如,可以将上文的 app_meta_list改写为dict形式: ````yaml vars : app_meta_list: app1: name: ansible-spring-example1 path: ../ansible-spring-example1/ version: 0.0.1 install_path: /tmp app2: name: Example2 ... ```` 那么,对dict的循环方式是相同的,差别是,dict包含key-value。因此,使用时需进行调整: ````yaml - name: call to app1.yml ansible.builtin.include_tasks: file: include/app1.yml vars: app_meta: "{{ dict_item.value }}" with_list: "{{ app_meta_dict }}" loop_control: loop_var: dict_item ```` 使用何种形式完全自由,或依赖于其他环境提供的数据源情况而定。 ### 3.1.6 变量文件 (var_file) app_deploy.yml中,app_meta_list 变量保存在 该文件内,是否有办法将其提取至外部呢? Ansible提供了var_file的方法,可以将变量写入单独的变量文件,供Playbook加载使用。 将app_deploy.yml中的vars内容复制,放入新建文件/vars/app_meta_config.yml ````yaml app_meta_list: - name: ansible-spring-example1 path: ../ansible-spring-example1/ version: 0.0.1 install_path: /tmp jvm_args: - -Xms128M - -Xmx256M - name: example2 path: ../example2/ version: 0.0.1 install_path: /tmp jvm_args: - -Xms64M - -Xmx128M ```` 注意,变量定义文件内容需要顶格编写。 而在app_deploy.yml中,使用 var_files 引用该文件 ````yaml - hosts: localhost gather_facts: no vars_files: - vars/app_meta_config.yml ```` 运行playbook: ````bash $ ansible-playbook deploy/app_deploy.yml -vvv ... Read vars_file 'vars/app_meta_config.yml' ... ```` 可见,变量文件已经生效。 *注意:* > absible命令参数 -v 是指更丰富的输出信息(verbose),通常调试错误时,使用一个v就够了,最多是4个v(-vvvv)。 ### 3.2 Roles 相较于 include_tasks,Ansible 还提供了更加强大的复用组织结构:Roles. #### 3.2.1 概念 **Role** 的含义是 **角色**。在Ansible文档中,Roles定义如下: > Roles are units of organization in Ansible. 只字未提Roles的内涵是什么! 可以将Roles理解为目标机提供的服务能力,而这种服务能力可由Ansible来进行部署、管理。 更直白的讲,目标机上需要运行的哪一类服务,那么可以认为它具备哪个角色(的能力)。如: - MySQL: 目标机需提供MySQL服务 -> Ansible 进行数据库安装、备份。 - Redis:目标机需要提供Redis服务 -> Ansible 进行集群安装、配置。 - Spring APP: 目标机需要运行Spring APP -> Ansible 进行部署,启动。 自然,一个目标机可以同时提供多种服务,同时具备多个角色。 回想之前定义的 app_deploy.yml,不就是一个 Role么?它可以实现任意的Spring APP的编译、部署、启动工作。那么,如何将其转换为Role呢? #### 3.2.2 组织 Roles是Ansible代码的一种组织方式,它采用目录结构将一组代码组织在一起,典型的目录文件结构如下: ````bash roles/ common/ # this hierarchy represents a "role" tasks/ # main.yml # <-- tasks file can include smaller files if warranted handlers/ # main.yml # <-- handlers file templates/ # <-- files for use with the template resource ntp.conf.j2 # <------- templates end in .j2 files/ # bar.txt # <-- files for use with the copy resource foo.sh # <-- script files for use with the script resource vars/ # main.yml # <-- variables associated with this role defaults/ # main.yml # <-- default lower priority variables for this role meta/ # main.yml # <-- role dependencies library/ # roles can also include custom modules module_utils/ # roles can also include custom module_utils lookup_plugins/ # or other types of plugins, like lookup in this case webtier/ # same kind of structure as "common" was above, done for the webtier role monitoring/ # "" fooapp/ # "" ```` 上图中可以看到: - Role需要放在 roles目录下。 - roles/common 目录代表一个Role:common - Role目录内包括 - tasks: 包含playbook tasks 文件,main.yml 是其入口文件。 - vars: 包含本Role使用的变量。 - files: 包含本Role使用的文件。 - templates: 包含本Role使用的Jinja2模板文件。 - 其他。。。(暂不考虑) 这样看来,实现一个 role 很简单。只需要将所需的资源文件整理好即可。 #### 3.2.3 实现app_v1 Role 首先按照目录结构建立: ```` roles └── app_v1 ├── tasks    │   └── main.yml    └── templates       ├── start.sh.j2       └── stop.sh.j2 ```` 其中: - 将include/app1.yaml 复制 到tasks/main.yml - 将templates/*.sh.j2复制到templates/并改名为 start.sh.js 和 stop.sh.j2 再回想一下app_v1 中的内容: - main.yml 实现编译并安装一个spring app功能 - mvn package - copy jar - templating start/stop.sh - restart app OK。只需要在传入 app_meta 变量调用即可。 #### 3.2.4 使用 role 为了使用app_v1 role,新建一个 app_call_role.yml,与之前类似,但不使用tasks 而使用 roles: ````yaml - hosts: localhost gather_facts: no vars_files: - vars/app_meta_config.yml roles: - role: app_v1 app_meta: "{{ app_meta_list [0] }}" ```` 和app_deploy.yml相比: - tasks 改成 roles - 使用vars 定义的变量 app_meta,改成了 role 的属性 app_meta。 - `app_meta_list` [0] 指代 app_meta_list 的第一个元素。 执行 playbook! 第一个role 很简单。 #### 3.2.5 执行多个Roles 当然也可以执行多个Roles,如,在 roles 后面添加 第二个 app : ````yaml roles: - role: app_v1 app_meta: "{{ app_meta_list [0] }}" - role: app_v1 app_meta: "{{ app_meta_list [1] }}" ```` 自然,也可以使用 循环 来调用,Ansible 提供了 include_role 模块,方法与 include_tasks类似。 将app_deploy.yaml中的 tasks复制过来: - 将include_tasks修改为 include_role, - 将 file: 修改为 name: ````yaml tasks: - name: call to role app_v1 include_role: name: app_v1 vars: app_meta: "{{ list_item }}" with_list: "{{ app_meta_list }}" loop_control: loop_var: list_item ```` 执行Playbook ! ## 3.3 改进模板 Anisble 的模板除了进行简单的字符串替换之外,还支持复杂的逻辑,Ansible 模板使用Jinja2模板技术,其模板语法与其保持一致。 ### 3.3.1 模板中的循环 在app_meta变量中,有一个属性jvm_args: ````yaml jvm_args: - -Xms128M - -Xmx256M ```` 在之前的模板中,尚未使用该参数,本节尝试在模板中实现。 jvm_args的作用是提供JVM的运行参数,其用法为: ````bash $ java -Xms128M -Xmx256M -jar some.jar ```` 在J2模板文件中使用控制语句,其方式是使用 `{% %}`来表示。下面是循环控制的内容: ````bash {% for arg in app_meta.jvm_args %} {{ arg }} {% endfor %} ```` for 循环类似于 python 的 for 循环写法,for **varible** in **list**的形式。循环结束时,需要使用 endfor。 这将在结果文件中替换为: ````bash -Xmms128M -Xmx256M ```` 按照该方法修改start.sh.j2: ````bash JVM_ARGS="{% for arg in app_meta.jvm_args %} {{ arg }} {% endfor %}" nohup java $JVM_ARGS -jar ... ```` 运行Playbook ````bash $ ansible-playbook deploy/app_call_role.yml ```` 使用 ps 参看进程启动参数: ````bash $ ps -ef | grep ansible-spring-example1-0.0.1.jar ... 00:00:16 java -Xms128M -Xmx256M -jar ansible-spring-example1-0.0.1.jar ```` #### 3.3.2 使用filter join jvm_args 属于简单的字符串拼接,类似于字符串 join 的效果,是否有比for循环更简单的方法呢? Ansible 结合 Jinja2 提供了大量的 filter 过滤器,其中的 **join** 过滤器可实现该功能。 ````bash JVM_ARGS="{{ app_meta.jvm_args | join(' ') }}" ```` Ansible使用管道符 | 来引用filter,join filter语法很类似于Python的 join,上例中使用空格连接一个list元素。 filter 也可以直接用于 Ansbile 中。 ### 3.3.3 使用判断语句 假如 app 并没有定义jvm_args,那么会怎么样?很明显,模板的app_meta.jvm_args会找不到属性定义。一旦出现这种情况,应如何处理呢? Jinja2 同样提供了逻辑判断语句,if ... else。同样,需要使用 {% %} 将其包围,如: ````bash {% if app_meta.jvm_args is defined %} {{ app_meta.jvm_args | join(' ') }} {% endif %} ```` *注意:* > `is defined`表示检查该变量或属性是否存在。 > `is undefined` 自然是表示不存在。 > `is not defined` 同样表示不存在。 Jinja2 还支持逻辑关系 `and` `or` 可以组合成更复杂的判断。 ````bash {% if app_meta.jvm_args is defined and app_meta.jvm_args != None %} {{ app_meta.jvm_args | join(' ') }} {% endif %} ```` 这样,更新`start.sh.j2`之后,同时将 app_meta_config.yml 中的jvm_args注释掉,再次运行看是否会出错。 ````bash JVM_ARGS="{% if app_meta.jvm_args is defined %}{{ app_meta.jvm_args | join(' ') }}{% endif %}" ```` #### 3.3.4 使用filter default 使用 if 虽然能实现目的,但针对变量未定义这种情况,Ansible提供了更简洁的方法: ````bash JVM_ARGS="{{ (app_meta.jvm_args| default([''])) | join(' ') }}" ```` `app_meta.jvm_args | default([''])` 表示: - 对 app_meta.jvm_args使用 default过滤器。 - default过滤器检查app_meta.jvm_args是否已定义 - 当app_meta.jvm_args未定义时,返回缺省值 ['']这是一个空数组。 这之后,结果再使用 join 过滤器进行连接。 当然,也可以去掉括号,改写为: ````bash JVM_ARGS="{{ app_meta.jvm_args| default (['']) | join (' ') }}" ```` 运行Playbook 并检查: ````bash $ ps -ef | grep ansible-spring-example1-0.0.1.jar .... pts/4 00:00:10 java -jar ansible-spring-example1-0.0.1.jar ```` default 可以进行变量检查并赋予默认值,这极大的简化了变量的检查应用。 ## Part 4: Inventory 在前面的三部分中,只使用了Localhost作为目标机,Ansible当然支持多目标机的管理,但与localhost相比,其他目标机在Playbook方面没有什么区别,仅仅在于登录方式的差别。 ### 4.1 访问目标机 为简便起见,本文仍使用本机作为测试目标机。 首先取得本机的 IP,本文中,IP 为: ` 192.168.219.149` 使用 ansible ping 来尝试连接: ````bash $ ansible 192.168.219.149 -m ping [DEPRECATION WARNING]: Ansible will require Python 3.8 or newer on the controller starting with Ansible 2.12. Current version: 3.6.9 (default, Jun 29 2022, 11:45:57) [GCC 8.4.0]. This feature will be removed from ansible- core in version 2.12. Deprecation warnings can be disabled by setting deprecation_warnings=False in ansible.cfg. [WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all' [WARNING]: Could not match supplied host pattern, ignoring: 192.168.219.149 ```` Ansible 提示找不到这台主机,信息中有: > `provided hosts list is empty, only localhost is available` 上面的命令中,确实没有提供任何 hosts list,那么应该如何指定呢? #### 4.1.1 提供 hosts 信息 编写一个example_hosts.yml: ````yaml all: hosts: first_host: ansible_host: 192.168.219.149 ```` example_hosts.yml中定义了一个主机:`first_host`,它的ip地址是:`192.168.219.149`。 使用ping命令访问它: ````bash $ ansible first_host -m ping -i example_hosts.yml ```` 在ansible 命令中,指定了 目标机`first_host`,使用 `-i example_hosts.yml` 指定了主机配置文件。ansible 将从该文件中查找 `first_host` 并使用定义的信息来访问它。 命令执行结果: ```` first_host | UNREACHABLE! => { "changed": false, "msg": "Failed to connect to the host via ssh: Warning: Permanently added '192.168.219.149' (ECDSA) to the list of known hosts.\r\nwangjf@192.168.219.149: Permission denied (publickey,password).", "unreachable": true } ```` 从错误信息中可以发现,`wangjf@192.168.219.149: Permission denied (publickey,password)`,无法登录该主机,通过ssh访问时需要提供密码或公钥才行。 #### 4.1.2 使用时输入密码 ansible 提供了在命令行输入密码的方法: ````bash $ ansible first_host -m ping -i example_hosts.yml -k SSH password: first_host | SUCCESS => { "ansible_facts": { "discovered_interpreter_python": "/usr/bin/python" }, "changed": false, "ping": "pong" } ```` 使用 `-k`参数,按提示输入密码,ping 命令成功。 #### 4.1.3 配置密码 显然,每次执行命令时输入密码是不可行的。需要在 hosts 定义中补充信息: ````yaml all: hosts: first_host: ansible_host: 192.168.219.149 ansible_user: your user name ansible_password: your password ```` 上文的 `your user name / password` 需要输入可用的用户名密码信息。 之后再尝试执行ping ````bash $ ansible first_host -m ping -i example_hosts.yml first_host | SUCCESS => { "ansible_facts": { "discovered_interpreter_python": "/usr/bin/python" }, "changed": false, "ping": "pong" } ```` Ping 成功! *注意:* > 如访问失败,可能是因为主机未信任对方密钥所致,可修改ansible配置文件来解决该问题: ````bash $ sudo vi /etc/ansible/ansible.cfg ... # uncomment this to disable SSH key host checking host_key_checking = False ... ```` > 将host_key_checking = False 注释去掉。 #### 4.1.4 执行Playbook 既然已经有了 first_host,就可以对该目标机执行playbook了。再次考察app_call_role.yml: ````yaml - hosts: localhost ```` 第一行hosts指定了目标机的名称,将其替换为 `first_host`即可: ````yaml - hosts: first_host ```` 执行app_call_role.yml: ````bash $ ansible-playbook deploy/app_call_role.yml -i example_hosts.yml ```` 执行成功! ### 4.2 host访问控制 在配置文件中使用明文密码毕竟是不安全的,ansible 提供了其他更好的访问控制手段。 #### 4.2.1 使用密文 Ansible提供了 Vault 工具进行敏感信息的加密。具体可参见其文档。 > 不建议为了密码问题引入Vault, 实际使用中还有更好的免密码登录方案。 #### 4.2.2 使用public-key 免密码访问 SSH提供密钥免密码登录的功能,Ansible也可通过该方式登录。 免密登录方式原理很简单: - 控制机生成一组密钥 - 将控制机密钥文本保存在目标机的`.ssh/authorized_keys`目录下。 操作过程如下: ````bash $ ssh-keygen # 按照提示一路回车即可 $ ssh-copy-id username@host # 指定登录的username 和 主机地址 # 按提示输入密码即可 ```` 执行成功后,就可以去掉example_hosts.yml中用户名、密码信息了。 执行 ping 或 playbook 进行验证。 ### 4.3 组织 hosts Ansible使用**Inventory**术语来表示目标机的集合,其管理重点在于如何**组织**大量的hosts。 Anisble提供了很灵活的组织形式来支持多角度管理。 #### 4.3.1 group 最简单的组织方式是分组,按照相同的需求进行分组,比如,现有一个环境,部署如下: - 服务: - MySQL服务: 两台,高可用。 - Spring 应用服务: 三台,负载均衡 - Redis 集群:三台 - Elasticsearch集群: 三台 - Ngnix服务:两台,高可用 - 服务器: - 5 台 物理机(或虚拟机) 这时,可以将这几台服务器进行分组, ````yaml all: hosts: server1 : server2 : server3 : server4 : server5 : children : es_servers: hosts: server1: server2: server3: mysql_servers: hosts: server1: server2: redis_cluster: hosts: server3: server4: server5: nginx_servers: hosts: server3: server4: spring_app_servers: hosts: server2: server4: server5 ```` 以上划分了5个group,group 可以直接用于 playbook 中,与Roles 结合则效果更佳: ````yaml - hosts: es_servers roles: - role: es_server - hosts: mysql_servers roles: - role: mysql_server .. ```` 对 es_servers中的目标机,执行 es_server role(如:安装elasticsearch, 配置集群), 对 mysql_servers 同样可完成数据的安装、数据恢复等一系列动作。 这样,面对变化时,会很从容,如部署调整为 5台服务器形成 ES 集群,那么只需要在 hosts 文件中假如其他服务器即可。 #### 4.3.2 hosts 变量 Ansible 同样可以在host定义上添加变量,这样在playbook里就可以访问它。 例如,ES 集群需要定义一个名字,这个名字就可定义在host变量里: ````yaml ... es_servers: hosts: server1: vars: cluster_name: my_es_cluster server2: vars: cluster_name: my_es_cluster server3: vars: cluster_name: my_es_cluster ```` 我们知道,同一个ES集群的名字是相同,因此,可以使用简单的写法: ````yaml ... es_servers: hosts: server1: server2: server3: vars: cluster_name: my_es_cluster ```` 同样的,变量也可以定义成复杂对象(dict或list)。 > 当然,也可以将参数定义在 vars文件中,供role来使用,但当出现不同服务器的变量不同时,这种方式就不适用了。 #### 4.3.3 使用 hostvars 变量 Ansible 提供了内置变量hostvars来保存所有inventory相关的信息 使用 hostvavrs ['host_name'] 就可以访问到相应的host变量。 比如,需要使用server1 的地址: ```` {{ hostvars ['server1'].ansible_host }} ```` 但在大多数场景下,需要得到当前目标机的信息,比如,在ES配置文件中有一些内容: ````yaml # Use a descriptive name for the node: # node.name: node # ... # Set the bind address to a specific IP (IPv4 or IPv6): # # network.host: 127.0.0.1 ```` `node.name` 通常使用服务器的主机名。`network.host` 指定服务绑定的地址。这俩项配置都需要使用目标机自身的信息。 那么,如何得到当前执行task的目标机信息呢? ansible 提供了变量 **invntory_hostname**,该变量代表当前执行任务的主机名称,显然,通过该变量,可以索引到相应的信息。 ````yaml # Use a descriptive name for the node: # node.name: node-{{ inventory_hostname }} # ... # Set the bind address to a specific IP (IPv4 or IPv6): # network.host: {{ hostvars [inventory_hostname].ansible_host }} ```` 该模板执行后,内容变成: ````yaml # Use a descriptive name for the node: # node.name: node-first_host # ... # Set the bind address to a specific IP (IPv4 or IPv6): # network.host: 192.168.219.149 ```` #### 4.3.4 使用 groups 变量 Ansible 也提供了组信息变量。groups包含了所有组的host名称列表,其形式为: ````yaml groups: mysql_servers: - server1 - server2 es_servers: - server3 - server4 - server5 ... ```` 使用 `groups ['es_servers']` 即可取出`es_servers` 组内所有主机名。再配合 hostvar,就可以完成基于组的变量表达式。 ES 配置中有一项: ````yaml # --------------------------------- Discovery ---------------------------------- # # Pass an initial list of hosts to perform discovery when new node is started: # The default list of hosts is ["127.0.0.1", "[::1]"] # # discovery.zen.ping.unicast.hosts: ```` 此配置需要填写集群的全部服务器IP地址,配合groups可以轻松实现: - 通过 groups ['es_servers'] 取出所有主机名 - 使用循环 - 使用 hostvars 查找 host 的IP地址。 ````bash discovery.zen.ping.unicast.hosts: [ {% for host in groups['es_servers'] %} "{{ hostvars[host].ansible_host }}:9300", {% endfor %} ] ```` *注意:* > 实际使用中不能有换行,换行只是为了更好的看清代码逻辑。 执行模板后的结果: ````yaml discovery.zen.ping.unicast.hosts: [ 192.168.1.1:9300 , 192.168.1.2:9300, ] ```` 注意到在第二个信息后,多了一个**`,`**,这是因为每次循环都会有一个。如何修正呢? 假如使用的是其他语言,则修正的思路是: ````java int i = 0 ; for (String host :groups ['es_servers']) { if (i != 0) print (", ") ; print (hostvars [host].ansible_host + ':9300') ; i ++ ; } ```` 那么,在ansible 中如何获取 i 呢?Ansible支持循环中的顺序号,可以使用 loop.index 来替代 i. ````bash discovery.zen.ping.unicast.hosts: [ {% for host in groups['es_servers'] %} {% if loop.index > 1 %} , {% endif %} {{ hostvars[host].ansible_host }}:9300, {% endfor %} ] ```` *注意:* > loop.index 是从1 开始的,如果需要使用0开始的序号,可使用loop.index0 还可以使用 loop.first 来判断是否是第一元素: ````bash {% if not loop.first %} ```` *注意:* > loop.first 是 Boolean类型,因此,使用逻辑not。 ## Part 5: Spring App Role 结合前4部分的内容,本章将进一步完善Spring App Role。 ### 5.1 环境信息 Spring 除了部署Jar之外,还有很重要的配置文件: application.yml 或 bootstrap.yml , 这些文件包含很多与运行环境相关的信息,比如: - Spring 端口号 - MySQL 服务器的地址,端口,用户名,密码 - ES 服务器的信息 如果是Spring cloud,还需要配置注册中心等环境信息。 幸运的是,Inventory中可以有效的包含这些信息。 > 本文仅介绍了部署应用的过程,Inventory还可以有效的部署各类服务,而在配置服务时,也需要具备这些信息。因此,将这些信息编写在Inventory是一举两得。 ### 5.2 配置ES application.yml中已经配置了ES信息: ````yaml spring: data: elasticsearch: cluster-name: my_es_cluster cluster-nodes: 192.168.2.131:9300, 192.168.2.132:9300 ```` 利用之前得知识,很容易将其模板化: ````yaml spring: data: elasticsearch: cluster-name: {{ hostvars [groups ['es_servers'] [0]].cluster_name }} cluster-nodes: {% for host in groups['es_servers'] %}{% if loop.index > 1 %}, {% endif %}{{ hostvars[host].ansible_host }}:9300{% endfor %} ```` #### 5.2.1 配置es_servers信息 相应的,需要在example_hosts.yml 中补充es_servers主机信息: ````yaml all: hosts: first_host: ansible_host: 192.168.219.149 # ansible_user: your user name # ansible_password: your password virtual_server1: ansible_host: 192.168.2.131 virtual_server2: ansible_host: 192.168.2.132 virtual_server3: ansible_host: 192.168.2.133 children: es_servers: hosts: virtual_server1: virtual_server2: virtual_server3: vars: cluster_name: my_es_cluster ```` *注意:* > 由于多个组的host变量会叠加,因此,建议在vars中使用前缀,如:es_cluster_name,或,es.cluster_name。 #### 5.2.2 编写 application.yml 的template 应在打包之前将application.yml模板化,这样,打包时Spring会正确打包产生的文件。 将 app_v1复制一份,改为app_v2。修改main.yml ````yaml # 在mvn package之前添加 - name: templating application.yml ansible.builtin.template: src: "{{ app_meta.path }}/src/main/resources/application.yml.j2" dest: "{{ app_meta.path }}/src/main/resources/application.yml" backup: no ```` 而在 app_call_role.yml 中,需要将 app_v1 改为 app_v2。 执行Playbook! #### 5.2.3 使用delegate_to 上一节有一个 重大的 bug:template 在执行时,是将控制机的src文件模板化之后,将其传送至 **目标机** 的dest文件。那么,再执行 package的时候,当然使用的是原始的 application.yml! 也就是说,这个template任务需要在控制机,也就是**"本地(localhost)"**执行,如何做到呢? ````yaml - name: templating application.yml delegate_to: localhost ansible.builtin.template: src: "{{ app_meta.path }}/src/main/resources/application.yml.j2" dest: "{{ app_meta.path }}/src/main/resources/application.yml" backup: no ```` Ansible 提供了 **delegate_to** 来指定执行任务的host。通常,会使用localhost。 运行playbook 验证吧。 *注意:* > 当使用template时,原始的 application.yml会被覆盖,而且无法恢复。 #### 5.2.4 模板包含 在微服务盛行的环境下,系统被拆解成许多Spring App,可以想见,其中很多APP会需要使用ES的配置信息,为了在不同应用的配置中共享,可以采用include的方式。 模板包含的语法很简单: ````bash {% include 'es_config.yml.j2' %} ```` 将ES配置部分复制出来,存入 /app_v2/templates/es_config.yml.j2。然后在application.yml.j2中使用include替换即可。 *注意:* > Ansible会在templates目录下搜索模板文件,因此,无需考虑模板的相对路径问题。 ### 5.3 多个配置文件的处理 Spring 可以有多个配置文件,比如:applcation/bootstrap 等等。如果需要支持多个配置文件,那么需要怎么处理呢? #### 5.3.1 最简单的方法 利用之前的知识,最简单的方法是: - 在 app_meta中定义变量,包含多个模板文件 - template 使用循环。 修改 app_meta_config.yml,增加 templates变量: ````yaml app_meta_list: - name: ansible-spring-example1 ... templates: - {src: application.yml.j2, dest: application.yml} ```` 修改main.yml,在template task使用循环: ````yaml - name: templating application.yml delegate_to: localhost ansible.builtin.template: src: "{{ app_meta.path }}/src/main/resources/{{ tpl_item.src }}" dest: "{{ app_meta.path }}/src/main/resources/{{ tpl_item.dest }}" backup: no with_list : "{{ app_meta.templates }}" loop_control: loop_var: tpl_item ```` #### 5.3.2 使用 filter splitext 通常,模板文件的命名都是在源文件后补充后缀`.j2`,那么,是否可以计算出源文件的名字,而不再需要反复的写`{ src: ...yml.j2, dest: ...yml}`呢? Ansible 提供 splitext 过滤器,它可以将字符串拆分成两部分,再结合 first | last 等过滤器就可以实现目标。 首先使用 ansible 进行简单的测试: ````bash $ ansible localhost -m debug -a "msg={{'app.yml.j2' | splitext }}" localhost | SUCCESS => { "msg": "('app.yml', '.j2')" } $ ansible localhost -m debug -a "msg={{'app.yml.j2' | splitext | first }}" localhost | SUCCESS => { "msg": "app.yml" } ```` 这样,可以将app_meta的dest删掉了: ````yaml app_meta_list: - name: ansible-spring-example1 ... templates: - application.yml.j2 ```` 再修改template task中的 dest: ````yaml - name: templating application.yml delegate_to: localhost ansible.builtin.template: src: "{{ app_meta.path }}/src/main/resources/{{ tpl_item }}" dest: "{{ app_meta.path }}/src/main/resources/{{ tpl_item | splitext | first }}" backup: no with_list : "{{ app_meta.templates }}" loop_control: loop_var: tpl_item ```` #### 5.3.4 使用条件判断 显然,当 app_meta.templates 没有定义时,`with_list : "{{ app_meta.templates }}"`会出错。 一种解决办法是使用 default filter: ````yaml with_list : "{{ app_meta.templates | default ([]) }}" ```` 另一种办法是使用条件判断,仅当定义了templates才执行: ````yaml - name: templating application.yml delegate_to: localhost when: app_meta.templates is defined ... ```` *注意:* > when 可以不使用 `{{ }}` 。 #### 5.3.3 使用 lookup “既得陇,又望蜀” 这是人的通病,能否更进一步的简化呢? 既然模板文件都是 `.j2`结尾,是否可以在resources目录下检查是否存在这类文件呢? Ansbile 提供 lookup plugin 来实现这一功能。 首先使用 ansible 命令行进行测试: ````bash $ ansible localhost -m debug -a "msg={{ lookup('fileglob', '*.md' , wantlist=True) }}" localhost | SUCCESS => { "msg": [ "./README.md", "./README.en.md" ] } ```` 当前目录下的两个 .md 文件被提取出来。 *注意:* > 必须使用wantlist=True,否则返回结果为"./README.md, ./README.en.md" > 从 wantlist=True的写法来看,符合Python 函数调用的范式,因此lookup的实现应为Python函数调用。 由此,可以不再需要 templates, 改写 main.yml。 - 修改with list 的值。 ````yaml with_list : "{{ lookup ('fileglob', app_meta.path + '/src/main/resources/*.j2', wantlist=True) }}" ```` - 由于使用 wantList强制返回 list, 因此不需要在使用 when 判断了。 - 返回的文件名称是合法路径,src/dest不再需要拼接 ````yaml src: "{{ tpl_item }}" dest: "{{ tpl_item | splitext | first }}" ```` 由此,修改后的代码更为简洁,有效。 ## 总结 通过一步一步实现spring应用部署的粗浅Playbook,引入了Anasiblee的下列概念和内容: - ansible 命令 - ansible-playbook 命令 - 常用模块 - ping - debug - copy - shell - template - include_tasks - Jinja2模板 - if - for - include - loop.index - loop.first - 控制 - loop - with_list - with_dict - loop_control - when - delegate_to - Inventory - 基本定义 - group - Varibles 变量 - hostvars - groups - inventory_hostname - Roles - 目录结构 - roles - Filters 过滤器 - join - default - splitext - first - lookup - lookup ('fileglob') 没有涉及到的内容更多: - handler - become - block - register - tags - 常用模块 - file - user/group - yum/apt - systemctl / service - mysql - docker - uri - ... - 更多filters - combine - path - ... - lookup - Dynamic Inventory 使用Ansible的重点,已经在本文中均涉及到,关于Ansible的管理和组织能力,也有直观介绍,已经足够入门。 理解一个系统,应首先从概念入手,具备整体清晰的轮廓,其他的内容仅是工具。比如,数量繁多的内置模块和更多的(ansible-galaxy)社区模块,在需要的时候再去查阅文档即可(如果没有现成的,那么就自己来写吧)。 一个好的项目,必然是概念清晰、结构简单的。这里的所说的简单,并非难度低或不够精巧,而是指容易理解,能用简单的几句话概括,知道它的构成、整体流程,能做什么,不能做什么,关键点是什么,如果做不到,那么很可能这个项目并不好用。 Ansible可以用几句话来概括: * Ansible 使用 SSH 在多个主机上执行任务,任务是以Python脚本形式运行的。 * Ansible 提供 多个内置模块和更多的社区扩展模块,涵盖绝大部分系统、应用的安装、设置、使用。 * Ansible 采用J2 模板作为其模板语言,可套用模板,动态生成各类文件,如:配置文件、脚本等。 * Ansible 提供结构化的主机(Inventory)和脚本(Role)组织形式。 * Ansible 通常使用脚本形式(Playbook)执行一组命令。 由此可以想见: * 要想使用Ansbile,主机必须要有SSH和Python。 * 要想尽快熟悉Ansible,应具备一定的Linux命令、shell脚本知识。 * 当需要执行某项任务时,首先检查是否有现成的模块,比如,要操作docker,应使用docker模块。MySql要用Mysql模块。这样事半功倍。 * 当需要动态生成配置文件时,采用J2模板,并依据需求进一步学习相应的模板语言和技巧。 * 结合Inventory、Role来构建可复用的脚本(Playbook)。 如感兴趣,则建议几个方向进一步学习: - filter:ansible提供大量filter,用的好则事半功倍。 - 与集合(list/dict)相关的变换、组合类filter - 与字符串处理相关的filter,如:path, url等。 - 与json/yaml相关的filter - lookup: - 数据查询类 - register: 将模块执行结果保存为变量。 - 理解 change - 理解loop情况下的register结果 - block: - 理解block原理 - 理解block异常处理 - rescue - always - facts: - 理解facts - 利用facts - 并行:提高执行效率 - 常用模块:这些模块用到再查看官方手册即可 - yum/apt/service/systemctl/user/group等系统操作类 - uri等执行Web API的模块 - 专项模块 - mysql - docker - k8s - AWS - redis