Menu

  • Home
  • Work
    • Cloud
      • Virtualization
      • IaaS
      • PaaS
    • Java
    • Go
    • C
    • C++
    • JavaScript
    • PHP
    • Python
    • Architecture
    • Others
      • Assembly
      • Ruby
      • Perl
      • Lua
      • Rust
      • XML
      • Network
      • IoT
      • GIS
      • Algorithm
      • AI
      • Math
      • RE
      • Graphic
    • OS
      • Linux
      • Windows
      • Mac OS X
    • BigData
    • Database
      • MySQL
      • Oracle
    • Mobile
      • Android
      • IOS
    • Web
      • HTML
      • CSS
  • Life
    • Cooking
    • Travel
    • Gardening
  • Gallery
  • Video
  • Music
  • Essay
  • Home
  • Work
    • Cloud
      • Virtualization
      • IaaS
      • PaaS
    • Java
    • Go
    • C
    • C++
    • JavaScript
    • PHP
    • Python
    • Architecture
    • Others
      • Assembly
      • Ruby
      • Perl
      • Lua
      • Rust
      • XML
      • Network
      • IoT
      • GIS
      • Algorithm
      • AI
      • Math
      • RE
      • Graphic
    • OS
      • Linux
      • Windows
      • Mac OS X
    • BigData
    • Database
      • MySQL
      • Oracle
    • Mobile
      • Android
      • IOS
    • Web
      • HTML
      • CSS
  • Life
    • Cooking
    • Travel
    • Gardening
  • Gallery
  • Video
  • Music
  • Essay

Ansible学习笔记

18
Apr
2018

Ansible学习笔记

By Alex
/ in Linux
0 Comments
简介

Ansible是一个自动化IT工具,能够配置系统、部署软件、编排复杂的IT任务(例如CD、零停机滚动更新)。

Ansible默认通过 SSH 协议管理,在管理机需要Python2.7环境,在托管机上需要Python 2环境。

安装
Ubuntu
Shell
1
2
3
4
5
6
7
8
# 下面的软件包在老版本Ubuntu上叫做python-software-properties
sudo apt-get install software-properties-common
sudo apt-add-repository ppa:ansible/ansible
sudo apt-get update
sudo apt-get install ansible
 
# 被管理的主机上需要安装Python2
sudo apt-get install python-minimal
Pip
Shell
1
/home/alex/Python/3.5.1/bin/pip install ansible 
配置

Ansible支持从多个位置读取配置选项,包括环境变量、命令行参数、名为ansible.cfg的ini文件。

ini文件的搜索顺序为:

  1. 环境变量ANSIBLE_CONFIG指定的位置
  2. 当前目录的ansible.cfg
  3. ~/.ansible.cfg
  4. /etc/ansible/ansible.cfg

配置文件中以#或者;开头的行为注释。

配置项说明
/etc/ansible/ansible.cfg
INI
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
[defaults]
; 主机清单文件位置
inventory      = /etc/ansible/hosts
; 远程SSH端口
remote_port    = 22
; SSH超时
timeout = 10
; 远程用户
remote_user = root
; SSH身份验证使用的私钥位置
private_key_file =/home/alex/Documents/puTTY/gmem.key
; 禁止由于不在known_hosts中导致的警告
; 等价于 export ANSIBLE_HOST_KEY_CHECKING=False
host_key_checking = False
; 和远程主机通信时,并行进程的数量
forks = 10
; 角色根目录
roles_path = /app/tops/roles
; 主机清单
inventory = /app/tops/host_vars/all.ini
; 控制Fact(远程系统变量)收集行为:
;   implicit  每次执行剧本,都收集
;   smart  避免不必要的收集,节约时间
gathering = smart
; Fact存储方式
fact_caching = jsonfile
; 连接到Fact存储的方式,对于jsonfile,需要指定一个目录
fact_caching_connection = ./.cache
; Fact缓存过期时间
fact_caching_timeout = 3600
; 剧本的标准输出回调,决定输出的显示方式
;   json
;   debug
;   skippy
stdout_callback = debug
; 额外启用的回调,从2.0开始Ansible的所有回调插件可用,但是默认是禁用状态。该设置让你可以指定哪些回调插件启用
;   timer 这个回调插件可以计算整个 playbook 的运行时间
;   mail 这个回调插件可以实现发送邮件的功能
;   profile_roles 这个插件是在执行中添加耗时
;
callback_whitelist = profile_tasks

全部可用配置项请参考:https://ansible-tran.readthedocs.io/en/latest/docs/intro_configuration.html

入门
主机清单

所谓主机清单(Host Inventory)是一个配置文件,来指定需要被管理的主机列表。主机清单的默认位置在/etc/ansible/hosts。

组

所有主机都属于 all组,此外可以自定义组:

/etc/ansible/hosts
INI
1
2
3
4
5
6
7
8
; 方括号内的是组名,每个主机都可以属于多个组
[k8s]
xenial-100.gmem.cc
xenial-101.gmem.cc
xenial-102.gmem.cc
xenial-103.gmem.cc
xenial-104.gmem.cc
xenial-105.gmem.cc
组的组

一个组,可以作为另外一个组的成员:

INI
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
; 组1
[atlanta]
host1
host2
 
; 组2
[raleigh]
host2
host3
 
; 组的组    该关键字表示定义组的成员
[southeast:children]
atlanta
raleigh
 
; 父组的变量
[southeast:vars]
some_server=foo.southeast.example.com
halon_system_timeout=30
self_destruct_countdown=60
escape_pods=2 
通配主机
INI
1
2
3
4
5
[vms]
xenial-[100-200].gmem.cc
;    1:50等价
www[01:50].example.com
db-[a:f].example.com
主机变量
INI
1
2
3
4
; 可以为主机设置别名,并指定主机变量
; 别名  ansible_开头的预定义变量
;                       SSH端口                SSH IP地址   通过SSH连接
jumper ansible_ssh_port=5555 ansible_ssh_host=192.168.1.50 ansible_connection=ssh

变量可以在Playbook(剧本)中引用。

组变量

除了主机变量,还可以定义属于整个组的变量:

INI
1
2
3
4
5
6
7
8
[atlanta]
host1
host2
 
; 组名    这个关键字表示该段定义的是变量
[atlanta:vars]
ntp_server=ntp.atlanta.example.com
proxy=proxy.atlanta.example.com
独立文件中的变量

在清单中定义所有变量不是最佳实践,你可以将它们放在独立的YAML中,这些YAML会和清单文件关联。

假设清单文件 /etc/ansible/hosts中有一个主机xenial-100,它属于两个组master、worker,则以下三个文件中定义的变量可以被该主机使用:

/etc/ansible/group_vars/master     组变量
/etc/ansible/group_vars/worker     组变量
/etc/ansible/host_vars/xenial-100  主机变量

上述三个文件,还可以创建为目录,并在里面定义任意数量的文件,在这些文件里面定义变量。只需要改目录名和组/主机名一致即可:

/etc/ansible/group_vars/master/sysctl
/etc/ansible/group_vars/master/connection

Ansible 1.2+,group_vars、host_vars可以放在:

  1. inventory文件所在目录
  2. playbook文件所在目录

如果两个目录下都有,则playbook覆盖inventory。

建议将inventory和变量一起纳入版本控制管理。

内置魔法变量
变量 说明
ansible_ssh_host

将要连接的远程主机名.与你想要设定的主机的别名不同的话,可通过此变量设置

ansible_ssh_port ssh端口号
ansible_ssh_user ssh 用户名
ansible_ssh_pass ssh 密码,不安全,建议使用 --ask-pass 或 SSH 密钥
ansible_sudo_pass sudo 密码
ansible_sudo_exe sudo 命令路径
ansible_connection

与主机的连接类型.比如:local, ssh 或者 paramiko

Ansible 1.2 以前默认使用 paramiko.1.2 以后默认使用 'smart','smart' 方式会根据是否支持 ControlPersist, 来判断'ssh' 方式是否可行

ansible_ssh_private_key_file ssh 使用的私钥文件.适用于有多个密钥,而你不想使用 SSH 代理的情况
ansible_shell_type 目标系统的shell类型.默认情况下,命令的执行使用 'sh' 语法,可设置为 'csh' 或 'fish'
ansible_python_interpreter

目标主机的 python 路径.适用于的情况: 系统中有多个 Python, 或者命令路径不是"/usr/bin/python",比如 \*BSD, 或者 /usr/bin/python
不是 2.X 版本的 Python.我们不使用 "/usr/bin/env" 机制,因为这要求远程用户的路径设置正确,且要求 "python" 可执行程序名不可为 python以外的名字(实际有可能名为python26)

inventory_hostname 当前(正在Play中迭代的)主机在主机清单中的名字
group_names 当前主机所属的组名列表
groups 当前主机清单中所有组的名字,到组包含的主机列表的字典
hostvars 当前主机清单中所有主机的名字,到主机所有变量的字典
playbook_dir 传递给ansible-playbook命令行的剧本文件的路径
role_name 当前正在执行的角色名称
role_path 当前正在执行的角色所在目录的路径
ansible_facts inventory_hostname的所有事实数据,通常又setup模块在play中自动收集,不过任何模块都可以返回事实
ansible_local inventory_hostname的所有收集/缓存的本地事实(local fact)数据
即席任务

使用ansible命令可以群发即席的命令给一群主机: ansible <host-pattern> [options],命令示例:

Shell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#       匹配目标主机的模式     执行的模块        传递给模块的参数   进一步限制主机范围
ansible <pattern_goes_here> -m <module_name> -a <arguments>  -l <group-or-@file>
# 示例
ansible webservers -m service -a "name=httpd state=restarted"  -l @hosts.txt
 
 
#       组   模块    登陆到机器的用户  改变身份                   询问密码
ansible all -m ping -u root        --sudo --sudo-user batman  --ask-sudo-pass
 
# 在所有主机上执行Shell命令
ansible all -u root -a "/bin/echo hello"
 
# 拷贝当前主机上的文件/etc/hosts到所有k8s组中的主机
ansible k8s -m copy -a "src=/etc/hosts dest=/tmp/hosts"
 
# 全部重启                            并发10个进程来执行
ansible -u root lb -a "/sbin/reboot" -f 10
 
# 额外的角色搜索路径
roles_path = /opt/mysite/roles:/opt/othersite/roles
主机模式
模式 说明
all * 匹配所有主机
192.168.1.*
*.example.com
局部通配符
webservers[0-25] 范围匹配
~(web|db).*\.example\.com 正则式匹配
webservers:dbservers 或
webservers:!phoenix 非
webservers:dbservers:&staging:!phoenix 更加复杂的逻辑操作
使用剧本

为了避免重复输入命令,Ansibel提供了Playbook脚本功能,脚本文件的格式为YAML。调用脚本的语法如下:

Shell
1
2
#                剧本文件     并发度
ansible-playbook deploy.yml  -f 10

下面是一个脚本示例:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
---
# 主机名、组名、或者all
- hosts: web
  # 定义变量
  vars:
    http_port: 80
    max_clients: 200
  # 远程登陆用户
  remote_user: root
  # 需要在远程主机上执行的任务列表,安装定义的顺序依次执行
  tasks:
  - name: ensure apache is at the latest version
    # 安装软件
    yum: pkg=httpd state=latest
    # 参数也可以作为子元素
    yum:
      pkg: httpd
      state: lastest
  
  - name: Write the configuration file
    # 写入配置文件
    template: src=templates/httpd.conf.j2 dest=/etc/httpd/conf/httpd.conf
    # 发布事件
    notify:
    - restart apache
 
  - name: ensure apache is running
    service: name=httpd state=started
  # 事件监听器
  handlers:
    - name: restart apache
      service: name=httpd state=restarted
 
# 可以针对另外一组主机声明脚本
- hosts: k8s
使用模块

模块就是Ansible的命令。调用模块是可以指定不同的参数,就像调用Bash命令那样。

命令中 

使用 -m参数指定module的名字, -a参数指定module的参数,示例: 

Shell
1
2
3
#                   参数格式"name1=value1 name2=value2"
ansible all -m copy -a "src=/etc/hosts dest=/tmp/hosts"
ansible web -m yum -a "name=httpd state=present"
脚本中

在Playbook脚本中每个任务(Task的子元素)都对应一个模块调用。

常用模块

所有模块的文档参考:http://docs.ansible.com/ansible/modules_by_category.html

模块名称 说明
ping

测试一个节点有没有配置好,执行检查:

  1. 能不能SSH登陆
  2. python版本是否满足需求

没有参数

debug

打印调试信息,类似于echo。支持变量替换

示例: ansible -m debug -a "var=hostvars[inventory_hostname]"

playbook示例:

YAML
1
2
3
4
5
6
7
8
---
- hosts: k8s
  tasks:
    - name: test
      debug:
        msg: "{{ inventory_hostname }} has gateway {{ ansible_default_ipv4.gateway }}"
        # 也可以打印变量
        var: hostvars[inventory_hostname]["ansible_default_ipv4"]["gateway"]
copy

从当前的机器上复制文件到远程节点上,并且设置合理的文件权限。拷贝文件的时候,会先比较下文件的Checksum,如果相同则不会拷贝。

注意,目标目录必须存在,如果不存在可以预先使用file模块创建

示例:

YAML
1
2
3
4
5
6
7
8
- copy:
    src: /srv/myfiles/foo.conf
    dest: /etc/foo.conf
    owner: foo
    group: foo
    mode: 0644
    # 备份被替换的文件
    backup: yes
synchronize

同步当前机器上的目录到远程节点:

Shell
1
2
3
4
- name: sync .helm directory
  synchronize:
    src:  /home/alex/.helm
    dest: /root/.helm
template

从当前的机器上复制文件到远程节点上,并进行变量替换。变量使用 {{ }}包围。示例:

YAML
1
2
3
4
5
6
7
8
9
- template:
    src: etc/ssh/sshd_config.j2
    dest: /etc/ssh/sshd_config.j2
    owner: root
    group: root
    mode: '0600'
    # 校验拷贝的文件是否有效
    validate: /usr/sbin/sshd -t %s
    backup: yes
file

设置远程值机上的文件、软链接和文件夹的权限,或者创建、删除文件。示例:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 修改文件模式
- file:
    path: /etc/foo.conf
    owner: foo
    group: foo
    mode: 0644
    mode: "u=rw,g=r,o=r"
    mode: "u+rw,g-wx,o-rwx"
 
# 创建软链接
- file:
    src: /file/to/link/to
    dest: /path/to/symlink
    owner: foo
    group: foo
    state: link
 
# 创建新文件
- file:
    path: /etc/foo.conf
    state: touch
    mode: "u=rw,g=r,o=r"
 
# 创建目录
- file:
    path: /etc/some_directory
    state: directory
    mode: 0755
user

增、删、改Linux远程节点的用户账户,并为其设置账户的属性。示例:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 创建账户
- user:
    name: johnd
    comment: "John Doe"
    uid: 1040
    group: admin
# 删除账户
- user:
    name: johnd
    state: absent
    remove: yes
 
# 修改属性
- user:
    name: jsmith
    #创建SSH 私钥
    generate_ssh_key: yes
    ssh_key_bits: 2048
    ssh_key_file: .ssh/id_rsa
 
- user:
    name: james18
    shell: /bin/zsh
    groups: developers
    # 设置账户过期时间
    expires: 1422403387
yum

用来管理Redhat系(RHEL,CentOS,Fedora 21-)的Linux上的安装包。示例:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- name: 安装最新版本的包,已经安装则替换老版本
    yum:
      name: httpd
      state: latest
 
- name: 安传个指定版本
    yum:
      name: httpd-2.2.29-1.4.amzn1
      state: present
 
- name: 删除软件包
    yum:
      name: httpd
      state: absent
- name: 从本地目录安装
  yum:
    name: /usr/local/src/nginx-release-centos-6-0.el6.ngx.noarch.rpm
    state: present 
apt

用来管理Debain系Linux上的软件包。示例:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
- name: 更新仓库缓存并安装软件包foo
  apt:
    name: foo
    update_cache: yes
 
- name: 安装软件包但是不启动
  apt: name=apache2 state=present
  environment:
    RUNLEVLEL: 1
    http_proxy: http://10.0.0.1:8088
- name: 移除软件包
  apt: name=foo state=absent
 
- name: 安装指定版本的软件包
  apt:
    name: foo=1.00
    state: present
 
- name: 更新所有软件包到最新版本
  apt:
    upgrade: dist
 
- name: 仅仅执行apt-get update
  apt:
    update_cache: yes
 
- name: 安装Deb包
  apt:
    deb: /tmp/mypackage.deb
 
- name: 安装foo包的build依赖
  apt:
    pkg: foo
    state: build-dep
 
- name: 移除无用依赖包
  apt:
    autoremove: yes
 
- name: 安装多个软件包
  apt: name={{item}} state=installed
  with_items:
    - kubelet
    - kubectl
package

通用的包管理器,使用底层的操作系统包管理器来安装、删除、升级软件包

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 确保软件包安装
- name: Install ntpdate
  package:
    name: ntpdate
    state: present
 
# 确保软件包移除
- name: Remove the apache package
  package:
    name: "{{ apache }}"
    state: absent
 
# 确保软件包是最新版本
- name: Install the latest version of Apache and MariaDB
  package:
    name:
      - httpd
      - mariadb-server
    state: latest 
apt_key

添加或者删除APT key。示例:

YAML
1
2
3
4
5
6
7
8
9
10
11
- name: 从Key服务器添加一个Key
  apt_key:
    keyserver: keyserver.ubuntu.com
    id: 36A1D7869245C8950F966E92D8576A8BA88D21E9
 
- name: 从URL添加Key
  apt_key:
    url: https://ftp-master.debian.org/keys/archive-key-6.0.asc
    state: present
 
- name: Add an Apt signing key, will not download if present
apt_repository

管理APT仓库。示例:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 添加指定的仓库到系统源列表
- apt_repository:
    repo: deb http://archive.canonical.com/ubuntu hardy partner
    state: present
 
# 添加指定的仓库到系统源列表,存放在指定的文件中
- apt_repository:
    repo: deb http://dl.google.com/linux/chrome/deb/ stable main
    state: present
    filename: google-chrome
 
# 移除指定的仓库
- apt_repository:
    repo: deb http://archive.canonical.com/ubuntu hardy partner
    state: absent
 
# 从PPA安装仓库
- apt_repository:
    repo: ppa:nginx/stable
service

管理远程节点上的服务。示例:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 开服务
- service:
    name: httpd
    state: started
# 关服务
- service:
    name: httpd
    state: stopped
# 重起服务
- service:
    name: httpd
    state: restarted
# 重载服务
- service:
    name: httpd
    state: reloaded
# 设置开机启动
- service:
    name: httpd
    enabled: yes
shell

通过/bin/sh在远程节点上执行命令,支持$HOME和”<”, “>”, “|”, “;” and “&”。示例:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- name: test $home
  shell: echo "Test" > ~/tmp/test
 
 
# 多行命令
- hosts: k8s
  tasks:
    - name: Pull images
      shell: |
        docker pull $IMAGE_REPO/kube-apiserver-amd64:v1.10.2
        docker pull $IMAGE_REPO/kube-scheduler-amd64:v1.10.2
      args:
        # 指定工作目录
        chdir: somedir/
        # 指定使用的脚本解析器
        executable: /bin/bash
command 类似shell, 但是不支持$HOME和”<”, “>”, “|”, “;” and “&” 
git

下载Git仓库的内容到指定位置:

Shell
1
2
ansible webservers -m git \
  -a "repo=git://foo.example.org/repo.git dest=/srv/myapp version=HEAD"
set_fact

使用该模块,可以设置per-host的变量(事实)。这些变量可以被当前ansible-playbook调用的、后续的play使用

如果设置cacheable为yes,则记录在缓存中,允许后续playbook调用使用

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
- name: "设置多个变量,用空格分隔"
  set_fact: one_fact="something" other_fact="{{ local_var }}"
 
- name: "另外一种多变量设置风格"
  set_fact:
    one_fact: something
    other_fact: "{{ local_var * 2 }}"
    another_fact: "{{ some_registered_var.results | map(attribute='ansible_facts.some_fact') | list }}"
 
- name: "允许缓存变量"
  set_fact:
    one_fact: something
    other_fact: "{{ local_var * 2 }}"
    cacheable: yes 
systemd

控制Systemd服务

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 确保服务启动
- name: Make sure a service is running
  systemd:
    state: started
    name: httpd
 
# 确保服务停止
- name: Stop service cron on debian, if running
  systemd:
    name: cron
    state: stopped
 
# 发起daemon-reload并重启服务
- name: Restart service cron on centos, in all cases, also issue daemon-reload to pick up config changes
  systemd:
    state: restarted
    daemon_reload: yes
    name: crond
 
# 重新载入服务
- name: Reload service httpd, in all cases
  systemd:
    name: httpd
    state: reloaded
 
# 启用服务
- name: Enable service httpd and ensure it is not masked
  systemd:
    name: httpd
    enabled: yes
    # 被mask的Systemd单元无法启动
    masked: no
 
# 重新载入配置
- name: Just force systemd to reread configs (2.4 and above)
  systemd:
    daemon_reload: yes
modprobe

加载或卸载内核模块:

YAML
1
2
3
4
5
6
7
- name: ensure kernel modules
  modprobe:
    name: "{{ item }}"
    # absent 表示卸载
    state: present
  with_items:
    - "br_netfilter"
剧本详解

剧本比即席任务复杂而强大的多,它可以:

  1. 编排有序的执行过程,在多组机器间,来回有序的执行特别指定的步骤
  2. 可以同步或异步的发起任务

Playbook的格式是YAML,它由一个或多个plays组成,它的内容是一个以plays为元素的列表。

在每个Play中,一组机器被映射为定义好的角色。

Play中包含Tasks,每个Task典型情况下是对某个Ansible模块的调用。

下面是一个简单的例子:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# 只有一个Play,它包含目标、变量、任务列表信息
---
  # 针对的主机,可以是主机、组的Pattern。所有任务都针对相同的主机
- hosts: webservers
  # 可以定义变量
  vars:
    http_port: 80
    max_clients: 200
  # 变量可以放在外部文件中,例如敏感信息
  vars_files:
  - /vars/external_vars.yml
  # 以什么用户登陆执行
  remote_user: root
  # 支持sudo
  sudo: yes
  sudo_user: postgres
  # 任务列表,注意任务是串行执行的,只有前一个执行完毕(不管有多少主机),后一个才开始执行
  tasks:
    # 每个任务都具有名称
  - name: ensure apache is at the latest version
    yum: pkg=httpd state=latest
    # 每个Task都可以覆盖用户
    remote_user: yourname
  - name: write the apache config file
    # 老版本中,需要写作 action: template,限制直接写模块名即可
    template: src=/srv/httpd.j2 dest=/etc/httpd.conf
    # 如果发生了变动(对于这个模块来说,就是目标文件内容发生了变化)
    # 则执行
    notify:
    # 需要注意,如果有多个Task都通知需要执行此Handler,只会执行一次
    - restart apache
  - name: ensure apache is running
    service: name=httpd state=started
  - name: run this command and ignore the result
    # 对于shell或command,如果不关心退出码,可以
    shell: /usr/bin/somecommand || /bin/true
    # 或者
    ignore_errors: True
  - name: Copy ansible inventory file to client
    # 参数太长,可以换行编写
    copy: src=/etc/ansible/hosts dest=/etc/ansible/hosts
            owner=root group=root mode=0644
    #                                    引用变量
  - name: create a virtual host file for {{ vhost }}
    template: src=somefile.j2 dest=/etc/httpd/conf.d/{{ vhost }}
 
  # 供引用的、发生变动后执行的处理器。通常都用来重启服务
  handlers:
    - name: restart apache
      service: name=httpd state=restarted

需要注意:

  1. 如果一个 host 执行 task 失败,这个 host 将会从整个 playbook 的 rotation 中移除
  2. 每个 task 的目标在于执行一个 moudle, 通常是带有特定的参数来执行.在参数中可以使用{{变量}}
  3. 模块具有幂等性,多次执行,它只会做必要的动作,此外当远端系统被人改动时,可以重放 playbooks 达到恢复的目的。对于command、shell这样的模块,多次执行相当于反复调用命令,是否幂等取决于底层命令
Tasks

注意:很多字段Role也可以用。

剧本、角色中都可以定义Task。Task具有以下通用字段:

字段 说明
action 该任务执行的动作,通常翻译为C(模块)或Action插件
any_errors_fatal 任何主机上的单个未处理的错误被传递到所有主机级别,并导致剧本失败
args 辅助的为Task增加参数的方式,字典
async 如果C(动作)支持,异步的执行Task,该字段的值是异步最大运行时间
poll 异步任务的轮询间隔
become 执行此任务时是否需要改变运行用户
become_user 改变为什么用户
changed_when 用于修改Ansible默认changed状态的判断逻辑的表达式
check_mode 控制任务是否运行在check模式
connection 修改在目标主机上执行时使用的connection插件
debugger 基于任务结果状态进行调试
delay 重试Task的间隔,必须和until联用
retries 重试Task的次数,必须和until联用
delegate_facts 是否将Fact应用到被代理的主机
delegate_to 在指定的主机上,而非目标主机上执行任务
local_action 等价于: delegate_to: localhost
run_once 用于控制禁止遍历主机列表逐个执行任务,如果为True则Task仅在第一个主机上执行
diff 是否返回diff信息
environment 提供给Task的环境变量字典
failed_when 覆盖Ansible默认failed状态判断逻辑
ignore_errors 任务失败时是否忽略并继续Play,和connection errors无关
ignore_unreachable 忽略无法连接的主机并且继续Play
loop 迭代一组任务,每个条目默认存入名为item的变量
loop_control 用于控制循环行为,例如修改item变量的名字
name 任务标识符
no_log 是否隐藏信息
notify 当changed=true时执行的处理器
port 修改连接的默认端口
register 用于存放任务状态、模块返回数据的变量
remote_user connection plugin使用的用户
tags 为Task打标签
throttle 控制任务并发数量,也就是任务在同一时刻,最多在几个主机上并行执行
vars 变量列表
when 条件满足时才执行任务
Handlers

对于任何一个主机,一个特定的Handler只会执行一次,不管它被Notify了多少次

Handlers 会在 'pre_tasks', 'roles', 'tasks', 和 'post_tasks' 之间自动执行,你也可以手工立即调用Handler:

YAML
1
2
3
4
5
tasks:
   - shell: some tasks go here
   # 立即调用
   - meta: flush_handlers
   - shell: some other tasks
Include

编写一个巨大的Playbook文件,不利于维护。 可以将具有重用价值的部分独立编写,然后通过include引用。

include主要用于包含来自其它文件的Task,也可以包含来自其它文件的Play。Handlers本质上也是Task,亦可被引用。

包含Task

用于被引用的Task列表,形式如下:

tasks/foo.yml
YAML
1
2
3
4
5
6
---
- name: placeholder foo
  command: /bin/foo
 
- name: placeholder bar
  command: /bin/bar

在Playbook中,include上述列表的语法:

YAML
1
2
tasks:
  - include: tasks/foo.yml

include还支持多种写法: 

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 1.0引入的语法
tasks:
  - include: wordpress.yml
    vars:
        wp_user: timmy
        some_list_variable:
          - alpha
          - beta
          - gamma
 
# 1.4之后,可以使用下面的语法
 
tasks:
  - { include: wordpress.yml, wp_user: timmy, ssh_keys: [ 'keys/one.txt', 'keys/two.txt' ] }
传递变量

include的时候,可以传递变量,所谓参数化include:

YAML
1
2
3
4
5
6
# 
tasks:
                           # 变量列表,在被包含文件中引用变量  {{ wp_user }}  
  - include: wordpress.yml wp_user=timmy
  - include: wordpress.yml wp_user=alice
  - include: wordpress.yml wp_user=bob

除了显式传递的变量之外,在Play的vars段中声明的变量,均可在被包含的列表中引用。

包含Handler

include也可以出现在handlers段: 

YAML
1
2
handlers:
  - include: handlers/handlers.yml
包含Play

甚至,include还可以将Playbook包含到另外一个Playbook中,这种情况下,不支持传递变量:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Play
- name: this is a play at the top level of a file
  hosts: all
  remote_user: root
 
  tasks:
 
  - name: say hi
    tags: foo
    shell: echo "hi..."
 
# 来自其它文件的Plays
- include: load_balancers.yml
- include: webservers.yml
- include: dbservers.yml
Role 

使用Role,使组织Playbook的最佳方式。Roles 基于特定的文件结构,去自动的加载某些 vars_files,tasks 以及 handlers。基于 roles 对内容进行分组,使得我们可以容易地与其他用户分享 roles

考虑下面的Ansible项目结构:

Shell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 剧本
site.yml
webservers.yml
fooservers.yml
# 角色,可以被剧本引用
roles/
   common/
     # 部分目录可以不存在
     files/
     templates/
     tasks/
     handlers/
     vars/
     defaults/
     meta/
   webservers/
     files/          # copy、script模块引用的文件
     templates/      # template模块引用的模版
     tasks/          # main.yaml为该角色的主任务列表,main.yaml可以include该目录的其它文件中定义的任务
     handlers/       # main.yaml为该角色的Handler列表
     vars/           # 自动添加到引用该角色的Play中
     defaults/       # 为角色、该角色依赖的其它角色,定义默认变量
     meta/           # 依赖的其它角色

其中剧本webservers.yml的内容: 

YAML
1
2
3
4
5
6
---
- hosts: webservers
  # Play包含了两个角色
  roles:
     - common
     - webservers

对于该剧本包含的任何Role x:

  1. 如果 roles/x/tasks/main.yml 存在, 其中列出的 tasks 将被添加到 play 中
  2. 所有 include tasks 可以引用 roles/x/tasks/ 中的文件,不需要指明文件的路径
  3. 如果 roles/x/handlers/main.yml 存在, 其中列出的 handlers 将被添加到 play 中
  4. 如果 roles/x/vars/main.yml 存在, 其中列出的 variables 将被添加到 play 中
  5. 如果 roles/x/meta/main.yml 存在, 其中列出的 “角色依赖” 将被添加到 roles 列表中,需要1.3+
  6. 所有 copy tasks 可以引用 roles/x/files/ 中的文件,不需要指明文件的路径
  7. 所有 script tasks 可以引用 roles/x/files/ 中的脚本,不需要指明文件的路径
  8. 所有 template tasks 可以引用 roles/x/templates/ 中的文件,不需要指明文件的路径
参数化Role

包含角色时,可以指定参数:

YAML
1
2
3
4
5
- hosts: webservers
  roles:
    - common
    - { role: foo_app_instance, dir: '/opt/a',  port: 5000 }
    - { role: foo_app_instance, dir: '/opt/b',  port: 5001 }
条件性Role
YAML
1
2
3
4
5
---
 
- hosts: webservers
  roles:
    - { role: some_role, when: "ansible_os_family == 'RedHat'" }

对于Role 的每个Task,都会执行when中的判断,仅当结果为true,该Task才被包含到剧本中。

为角色添加Tag
YAML
1
2
3
4
5
---
 
- hosts: webservers
  roles:
    - { role: foo, tags: ["bar", "baz"] }

Tags 是一种实现部分运行 playbook 的机制

前/后置Task

如果你想在Role执行之前、之后,执行一些Task,使用下面的配置:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
---
 
- hosts: webservers
 
  pre_tasks:
    - shell: echo 'hello'
 
  roles:
    - { role: some_role }
 
  tasks:
    - shell: echo 'still busy'
 
  post_tasks:
    - shell: echo 'goodbye'

注意,使用了Role的Play,如果同时还定义了tasks段,则其中的任务都在Role中的Task结束后才执行。 

角色默认变量

你可以为角色、以及这些角色依赖(dependent,包含的其它,定义在角色的meta/main.yaml)的角色,设置默认变量。

这些变量在所有可用变量中拥有最低优先级,可能被其他地方定义的变量(包括 inventory 中的变量)所覆盖。

角色依赖

可以将其它角色,自动拉取到当前角色中。依赖的角色定义在 meta/main.yaml 中:

YAML
1
2
3
4
5
6
7
8
9
---
dependencies:
  #                   可以传递参数
  - { role: postgres, dbname: blarg, other_parameter: 12 }
  # 可以使用绝对路径引用Role
  - { role: '/path/to/common/roles/foo', x: 1 }
  # 甚至引用Git服务器、压缩包中的角色,可以指定友好角色名
  - { role: 'git+http://git.example.com/repos/role-foo,v1.1,foo' }
  - { role: '/path/to/tar/file.tgz,,friendly-name' }

我们知道,Role定义了完整的可执行实体,Playbook可以仅简单的引用、传递参数给它。

当Role依赖于其它Role时,被依赖的Role先执行,这个过程可以是递归的。 

默认情况下,被依赖的Role仅仅会执行一次。如果有两个Role同时依赖它,第二个依赖被忽略。要改变此行为,可以修改依赖规则:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
---
allow_duplicates: yes
dependencies:
- { role: tire }
- { role: brake }
 
 
# 在上述配置下
---
dependencies:
- { role: wheel, n: 1 }
- { role: wheel, n: 2 }
- { role: wheel, n: 3 }
- { role: wheel, n: 4 }
# 多次依赖
---
allow_duplicates: yes
dependencies:
- { role: tire }
- { role: brake }
# 执行序列:
# tire(n=1)
# brake(n=1)
# wheel(n=1)
# tire(n=2)
# brake(n=2)
# wheel(n=2)
# ...
#
模块嵌入到角色 

如果你自己开发了一个Ansible 模块,你甚至可以将它作为角色的一部分,进行分发。 

YAML
1
2
3
4
5
6
7
8
9
10
11
roles/
   my_custom_modules/
       library/
          module1
          module2
 
- hosts: webservers
  roles:
    - my_custom_modules
    - some_other_role_using_my_custom_modules
    - yet_another_role_using_my_custom_modules

这些自定义模块,在当前Role,以及后面执行的Role中可用。

Variables
Inventory中的变量

你可以为主机或分组定义变量,具体参考前面的章节。

Playbook变量

在Play中,你可以直接定义变量:

YAML
1
2
3
- hosts: webservers
  vars:
    http_port: 80
Role中的变量 

角色有vars、defaults目录,存放变量信息。

Jinja2语法

为了使用变量,你需要在Playbook以及它引用的文件中,使用Jinja模板。该模板语言使用 {{}}作为变量占位符: 

YAML
1
template: src=foo.cfg.j2 dest={{ remote_install_path }}/foo.cfg

和Go Template是相似的。

需要注意,YAML语法要求,如果值以{{  }}开头的话我们需要将整行用双引号包起来,防止错误的解释为YAML字典:

YAML
1
2
3
4
5
6
7
8
9
# 错误
- hosts: app_servers
  vars:
      app_path: {{ base_path }}/22
 
# 正确
- hosts: app_servers
  vars:
       app_path: "{{ base_path }}/22 
Jinja2过滤器

类似于Go Template的函数语法: {{ my_variable|default('my_variable is not defined') }}

Jinja2包含许多内置过滤器:https://jinja.palletsprojects.com/en/2.11.x/templates/#builtin-filters

注册结果为变量

Ansible模块通常会返回一个数据结构,你可以把模块调用的结果存放到一个变量中:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
- hosts: web_servers
 
  tasks:
 
     - shell: /usr/bin/foo
       # 注册结果为变量
       register: foo_result
       ignore_errors: True
 
     - shell: /usr/bin/bar
       # 使用变量进行判断
       when: foo_result.rc == 5
结果字段说明

这个数据结构包含的字段如下:

字段 说明
backup_file 某些操控文件的模块支持 backup=no|yes,这个字段存放备份文件的路径
changed bool,提示任务是否对目标进行了变更
diff 数组,说明执行任务前后的变化
failed bool,提示任务是否失败
invocation 对象,模块调用的细节信息
msg 一般性的、中继给用户的消息
rc 某些模块,例如raw shell command,会执行命令,这个字段存放命令退出码
results 数组。如果该字段存在,则提示任务是一个loop,该字段存放每次loop的结果数据结构
skipped bool,提示任务是否被跳过
stderr 标准错误
stderr_lines 标准错误,每行一个元素,构成数组
stdout 标准输出
stdout_lines 标准输出,每行一个元素,构成数组
变量访问语法 
YAML
1
2
3
4
# 方括号语法
{{ ansible_eth0["ipv4"]["address"] }}
# 点好导航语法
{{ ansible_eth0.ipv4.address }}
魔法变量

Ansible会自动提供给你一些变量,即使你并没有定义过它们,这些变量名是预留的,用户不应当覆盖它们:

  1. hostvars:可以让你访问其它主机的变量、包括哪些主机中获取到的facts
  2. group_names:当前主机所在所有群组的列表
  3. groups:inventory中所有群组/主机的列表。可以用来列举主机:
    1
    2
    3
    {% for host in groups['app_servers'] %}
       {{ hostvars[host]['ansible_eth0']['ipv4']['address'] }}
    {% endfor %}

所有魔法变量的列表,请参考上文。

命令行变量

可以在调用Ansible命令时传递变量:

Shell
1
2
3
4
5
6
7
8
9
10
11
# ---
 
# - hosts: '{{ hosts }}'
#   remote_user: '{{ user }}'
 
#   tasks:
#      - ...
 
ansible-playbook release.yml --extra-vars "hosts=vipers user=starbuck"
                             # 支持JSON
                             --extra-vars '{"pacman":"mrs","ghosts":["inky","pinky","clyde","sue"]}'
变量优先级

从高到低:

命令行-e ⇨

Inventory中定义的连接变量(例如ansible_ssh_user)⇨

Play中的变量、included的变量、Role中的变量⇨

Inventory中的其它变量 ⇨

Facts ⇨

Role默认变量 

Facts

这是一类变量,但是它们的值是Ansible自动检测出来的,而非你去指定的。

使用命令 ansible hostname -m setup可以获取Facts,数据量非常大:

JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
"ansible_all_ipv4_addresses": [
    "REDACTED IP ADDRESS"
],
"ansible_all_ipv6_addresses": [
    "REDACTED IPV6 ADDRESS"
],
"ansible_architecture": "x86_64",
"ansible_bios_date": "09/20/2012",
"ansible_bios_version": "6.00",
"ansible_cmdline": {
    "BOOT_IMAGE": "/boot/vmlinuz-3.5.0-23-generic",
    "quiet": true,
    "ro": true,
    "root": "UUID=4195bff4-e157-4e41-8701-e93f0aec9e22",
    "splash": true
},
"ansible_date_time": {
    "date": "2013-10-02",
    "day": "02",
    "epoch": "1380756810",
    "hour": "19",
    "iso8601": "2013-10-02T23:33:30Z",
    "iso8601_micro": "2013-10-02T23:33:30.036070Z",
    "minute": "33",
    "month": "10",
    "second": "30",
    "time": "19:33:30",
    "tz": "EDT",
    "year": "2013"
},
"ansible_default_ipv4": {
    "address": "REDACTED",
    "alias": "eth0",
    "gateway": "REDACTED",
    "interface": "eth0",
    "macaddress": "REDACTED",
    "mtu": 1500,
    "netmask": "255.255.255.0",
    "network": "REDACTED",
    "type": "ether"
},
"ansible_default_ipv6": {},
"ansible_devices": {
    "fd0": {
        "holders": [],
        "host": "",
        "model": null,
        "partitions": {},
        "removable": "1",
        "rotational": "1",
        "scheduler_mode": "deadline",
        "sectors": "0",
        "sectorsize": "512",
        "size": "0.00 Bytes",
        "support_discard": "0",
        "vendor": null
    },
    "sda": {
        "holders": [],
        "host": "SCSI storage controller: LSI Logic / Symbios Logic 53c1030 PCI-X Fusion-MPT Dual Ultra320 SCSI (rev 01)",
        "model": "VMware Virtual S",
        "partitions": {
            "sda1": {
                "sectors": "39843840",
                "sectorsize": 512,
                "size": "19.00 GB",
                "start": "2048"
            },
            "sda2": {
                "sectors": "2",
                "sectorsize": 512,
                "size": "1.00 KB",
                "start": "39847934"
            },
            "sda5": {
                "sectors": "2093056",
                "sectorsize": 512,
                "size": "1022.00 MB",
                "start": "39847936"
            }
        },
        "removable": "0",
        "rotational": "1",
        "scheduler_mode": "deadline",
        "sectors": "41943040",
        "sectorsize": "512",
        "size": "20.00 GB",
        "support_discard": "0",
        "vendor": "VMware,"
    },
    "sr0": {
        "holders": [],
        "host": "IDE interface: Intel Corporation 82371AB/EB/MB PIIX4 IDE (rev 01)",
        "model": "VMware IDE CDR10",
        "partitions": {},
        "removable": "1",
        "rotational": "1",
        "scheduler_mode": "deadline",
        "sectors": "2097151",
        "sectorsize": "512",
        "size": "1024.00 MB",
        "support_discard": "0",
        "vendor": "NECVMWar"
    }
},
"ansible_distribution": "Ubuntu",
"ansible_distribution_release": "precise",
"ansible_distribution_version": "12.04",
"ansible_domain": "",
"ansible_env": {
    "COLORTERM": "gnome-terminal",
    "DISPLAY": ":0",
    "HOME": "/home/mdehaan",
    "LANG": "C",
    "LESSCLOSE": "/usr/bin/lesspipe %s %s",
    "LESSOPEN": "| /usr/bin/lesspipe %s",
    "LOGNAME": "root",
    "LS_COLORS": "rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:su=37;41:sg=30;43:ca=30;41:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arj=01;31:*.taz=01;31:*.lzh=01;31:*.lzma=01;31:*.tlz=01;31:*.txz=01;31:*.zip=01;31:*.z=01;31:*.Z=01;31:*.dz=01;31:*.gz=01;31:*.lz=01;31:*.xz=01;31:*.bz2=01;31:*.bz=01;31:*.tbz=01;31:*.tbz2=01;31:*.tz=01;31:*.deb=01;31:*.rpm=01;31:*.jar=01;31:*.war=01;31:*.ear=01;31:*.sar=01;31:*.rar=01;31:*.ace=01;31:*.zoo=01;31:*.cpio=01;31:*.7z=01;31:*.rz=01;31:*.jpg=01;35:*.jpeg=01;35:*.gif=01;35:*.bmp=01;35:*.pbm=01;35:*.pgm=01;35:*.ppm=01;35:*.tga=01;35:*.xbm=01;35:*.xpm=01;35:*.tif=01;35:*.tiff=01;35:*.png=01;35:*.svg=01;35:*.svgz=01;35:*.mng=01;35:*.pcx=01;35:*.mov=01;35:*.mpg=01;35:*.mpeg=01;35:*.m2v=01;35:*.mkv=01;35:*.webm=01;35:*.ogm=01;35:*.mp4=01;35:*.m4v=01;35:*.mp4v=01;35:*.vob=01;35:*.qt=01;35:*.nuv=01;35:*.wmv=01;35:*.asf=01;35:*.rm=01;35:*.rmvb=01;35:*.flc=01;35:*.avi=01;35:*.fli=01;35:*.flv=01;35:*.gl=01;35:*.dl=01;35:*.xcf=01;35:*.xwd=01;35:*.yuv=01;35:*.cgm=01;35:*.emf=01;35:*.axv=01;35:*.anx=01;35:*.ogv=01;35:*.ogx=01;35:*.aac=00;36:*.au=00;36:*.flac=00;36:*.mid=00;36:*.midi=00;36:*.mka=00;36:*.mp3=00;36:*.mpc=00;36:*.ogg=00;36:*.ra=00;36:*.wav=00;36:*.axa=00;36:*.oga=00;36:*.spx=00;36:*.xspf=00;36:",
    "MAIL": "/var/mail/root",
    "OLDPWD": "/root/ansible/docsite",
    "PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
    "PWD": "/root/ansible",
    "SHELL": "/bin/bash",
    "SHLVL": "1",
    "SUDO_COMMAND": "/bin/bash",
    "SUDO_GID": "1000",
    "SUDO_UID": "1000",
    "SUDO_USER": "mdehaan",
    "TERM": "xterm",
    "USER": "root",
    "USERNAME": "root",
    "XAUTHORITY": "/home/mdehaan/.Xauthority",
    "_": "/usr/local/bin/ansible"
},
"ansible_eth0": {
    "active": true,
    "device": "eth0",
    "ipv4": {
        "address": "REDACTED",
        "netmask": "255.255.255.0",
        "network": "REDACTED"
    },
    "ipv6": [
        {
            "address": "REDACTED",
            "prefix": "64",
            "scope": "link"
        }
    ],
    "macaddress": "REDACTED",
    "module": "e1000",
    "mtu": 1500,
    "type": "ether"
},
"ansible_form_factor": "Other",
"ansible_fqdn": "ubuntu2.example.com",
"ansible_hostname": "ubuntu2",
"ansible_interfaces": [
    "lo",
    "eth0"
],
"ansible_kernel": "3.5.0-23-generic",
"ansible_lo": {
    "active": true,
    "device": "lo",
    "ipv4": {
        "address": "127.0.0.1",
        "netmask": "255.0.0.0",
        "network": "127.0.0.0"
    },
    "ipv6": [
        {
            "address": "::1",
            "prefix": "128",
            "scope": "host"
        }
    ],
    "mtu": 16436,
    "type": "loopback"
},
"ansible_lsb": {
    "codename": "precise",
    "description": "Ubuntu 12.04.2 LTS",
    "id": "Ubuntu",
    "major_release": "12",
    "release": "12.04"
},
"ansible_machine": "x86_64",
"ansible_memfree_mb": 74,
"ansible_memtotal_mb": 991,
"ansible_mounts": [
    {
        "device": "/dev/sda1",
        "fstype": "ext4",
        "mount": "/",
        "options": "rw,errors=remount-ro",
        "size_available": 15032406016,
        "size_total": 20079898624
    }
],
"ansible_nodename": "ubuntu2.example.com",
"ansible_os_family": "Debian",
"ansible_pkg_mgr": "apt",
"ansible_processor": [
    "Intel(R) Core(TM) i7 CPU         860  @ 2.80GHz"
],
"ansible_processor_cores": 1,
"ansible_processor_count": 1,
"ansible_processor_threads_per_core": 1,
"ansible_processor_vcpus": 1,
"ansible_product_name": "VMware Virtual Platform",
"ansible_product_serial": "REDACTED",
"ansible_product_uuid": "REDACTED",
"ansible_product_version": "None",
"ansible_python_version": "2.7.3",
"ansible_selinux": false,
"ansible_ssh_host_key_dsa_public": "REDACTED KEY VALUE"
"ansible_ssh_host_key_ecdsa_public": "REDACTED KEY VALUE"
"ansible_ssh_host_key_rsa_public": "REDACTED KEY VALUE"
"ansible_swapfree_mb": 665,
"ansible_swaptotal_mb": 1021,
"ansible_system": "Linux",
"ansible_system_vendor": "VMware, Inc.",
"ansible_user_id": "root",
"ansible_userspace_architecture": "x86_64",
"ansible_userspace_bits": "64",
"ansible_virtualization_role": "guest",
"ansible_virtualization_type": "VMware"

Facts可以直接在Playbook中引用,例如获取第一块磁盘的型号: {{ ansible_devices.sda.model }}

禁用Facts

你可以为Play禁用Fact收集:

YAML
1
2
- hosts: whatever
  gather_facts: no
设置Facts

一般情况下,Facts都是Ansible自动检测的,目标主机的信息。

你可以在目标主机下放置配置文件,硬编码一些变量值:

/etc/ansible/facts.d/perferences.fact
INI
1
2
3
[general]
asdf=1
bar=2

这将产生如下变量: 

YAML
1
2
3
4
5
6
7
8
9
10
# ansible <hostname> -m setup -a "filter=ansible_local"
 
"ansible_local": {
        "preferences": {
            "general": {
                "asdf" : "1",
                "bar"  : "2"
            }
        }
}

可以在剧本或模板中引用这种变量: {{ ansible_local.preferences.general.asdf }} 

Facts缓存 

在一个主机上运行的剧本中,引用另外一个主机中的变量,是可能的: {{ hostvars['asdf.example.com']['ansible_os_family'] }}

如果没有Facts缓存,则Ansible需要在当前主机和另外一台主机进行通信,获取必要的变量。这在大规模集群中,可能造成负担。

Facts缓存是一种分布式缓存,可以依托Jsonfile或Redis实现,解决上述问题。

条件
when语句

可以使用逻辑表达式、Jinja2过滤器,来编写when,在运行时决定Task是否执行:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
tasks:
  - name: "shutdown Debian flavored systems"
    command: /sbin/shutdown -t now
    when: ansible_os_family == "Debian"  
    # 使用过滤器
    when: result|success                
    when: ansible_os_family == "RedHat" and ansible_lsb.major_release|int >= 6
    # 取反
    when: not var0
    # 是否定义
    when: bar is not defined
    # 字符串包含
    when: "'reticulating splines' in output"
tasks:
    - command: echo {{ item }}
      # 在循环中使用
      with_items: [ 0, 2, 4, 6, 8, 10 ]
      when: item > 5

除了Task,你还可以在roles、include语句中使用when:

YAML
1
2
3
4
5
6
7
- include: tasks/sometasks.yml
  when: "'reticulating splines' in output"
 
 
- hosts: webservers
  roles:
     - { role: debian_stock_config, when: ansible_os_family == 'Debian' }
循环 

如果想在一个任务中干很多事,比如创建一群用户、安装很多包、或者重复一个轮询步骤直到收到某个特定结果,考虑使用循环支持。

标准循环

循环变量是简单值:

YAML
1
2
3
4
5
6
7
8
9
- name: add several users
  # 通过item引用当前循环变量
  user: name={{ item }} state=present groups=wheel
  # 循环
  with_items:
     - testuser1
     - testuser2
  # 可以引用变量定义的列表
  with_items: "{{somelist}}"

循环变量是字典:

YAML
1
2
3
4
5
- name: add several users
  user: name={{ item.name }} state=present groups={{ item.groups }}
  with_items:
    - { name: 'testuser1', groups: 'wheel' }
    - { name: 'testuser2', groups: 'root' }

循环变量是哈希表的条目:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
---
users:
  alice:
    name: Alice Appleworth
    telephone: 123-456-7890
  bob:
    name: Bob Bananarama
    telephone: 987-654-3210
 
 
 
tasks:
  - name: Print phone records
    #                   访问键             访问值
    debug: msg="User {{ item.key }} is {{ item.value.name }} ({{ item.value.telephone }})"
    with_dict: "{{users}}"
嵌套循环 
YAML
1
2
3
4
5
6
7
- name: give users access to multiple databases
  mysql_user: name={{ item[0] }} priv={{ item[1] }}.*:ALL append_privs=yes password=foo
  with_nested:
    # 外层变量
    - [ 'alice', 'bob' ]
    # 内层变量
    - [ 'clientdb', 'employeedb', 'providerdb' ]
子元素嵌套

对于变量定义:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
---
users:
  - name: alice
    authorized:
      - /tmp/alice/onekey.pub
      - /tmp/alice/twokey.pub
    mysql:
        password: mysql-password
        hosts:
          - "%"
          - "127.0.0.1"
          - "::1"
          - "localhost"
        privs:
          - "*.*:SELECT"
          - "DB1.*:ALL"
  - name: bob
    authorized:
      - /tmp/bob/id_rsa.pub
    mysql:
        password: other-mysql-password
        hosts:
          - "db1"
        privs:
          - "*.*:SELECT"
          - "DB2.*:ALL"

如果我们想给每个name创建系统用户,并且为每个用户的每个authorized创建密钥登陆,可以:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
- user: name={{ item.name }} state=present generate_ssh_key=yes
  # 遍历每个用户,创建用户
  with_items: "{{users}}"
 
#                          items.0表示当前外层变量  
#                                     items.1表示当前内层变量                          
- authorized_key: "user={{ item.0.name }} key='{{ lookup('file', item.1) }}'"
  with_subelements:
     # 外层是用户
     - users
     # 内层是当前用户的authorized列表(users[x].authorized)
     - authorized

类似的,创建MySQL用户

YAML
1
2
3
4
5
6
- name: Setup MySQL users
  mysql_user: name={{ item.0.user }} password={{ item.0.mysql.password }}
              host={{ item.1 }} priv={{ item.0.mysql.privs | join('/') }}
  with_subelements:
    - users
    - mysql.hosts
访问文件列表
YAML
1
2
3
4
5
6
7
8
9
10
11
12
---
- hosts: all
 
  tasks:
 
    # 确保目录存在
    - file: dest=/etc/fooapp state=directory
 
    # 拷贝所有匹配Pattern的文件
    - copy: src={{ item }} dest=/etc/fooapp/ owner=root mode=600
      with_fileglob:
        - /playbooks/files/fooapp/*
同时迭代两个列表
YAML
1
2
3
4
5
6
7
8
9
10
11
---
alpha: [ 'a', 'b', 'c', 'd' ]
numbers:  [ 1, 2, 3, 4 ]
 
 
 
tasks:
    - debug: msg="{{ item.0 }} and {{ item.1 }}"
      with_together:
        - "{{alpha}}"
        - "{{numbers}}"
访问列表的索引 
YAML
1
2
3
- name: indexed loop demo
  debug: msg="at array position {{ item.0 }} there is a value {{ item.1 }}"
  with_indexed_items: "{{some_list}}"
随机选取

可以从列表中随机选取一组值,然后循环:

YAML
1
2
3
4
5
6
- debug: msg={{ item }}
  with_random_choice:
     - "go through the door"
     - "drink from the goblet"
     - "press the red button"
     - "do nothing"
遍历整数序列 
YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
---
- hosts: all
 
  tasks:
 
    # create groups
    - group: name=evens state=present
    - group: name=odds state=present
 
    - user: name={{ item }} state=present groups=evens
      #              起      止      格式化item
      with_sequence: start=0 end=32 format=testuser%02x
 
    - file: dest=/var/stuff/{{ item }} state=directory
      #                             步进
      with_sequence: start=4 end=16 stride=2
 
 
    - group: name=group{{ item }} state=present
      with_sequence: count=4
Do-Until
YAML
1
2
3
4
5
6
7
8
- action: shell /usr/bin/foo
  # 结果注册为变量
  register: result
  # 循环判断
  until: result.stdout.find("all systems go") != -1
  # 最大重试次数和延迟
  retries: 5
  delay: 10
逐行处理 
YAML
1
2
3
- name: Example of looping over a command result
  shell: /usr/bin/frobnicate {{ item }}
  with_lines: /usr/bin/frobnications_per_host --param {{ inventory_hostname }}
异步和轮询

默认情况下playbook中的任务执行时会一直保持连接,直到该任务在每个节点都执行完毕。

对于长时间运行的任务,可以一起异步发动,然后进行轮询,来判断是否结束。

YAML
1
2
3
4
5
6
7
8
9
10
11
- hosts: all
  remote_user: root
 
  tasks:
 
  - name: simulate long running op (15 sec), wait for up to 45 sec, poll every 5 sec
    command: /bin/sleep 15
    # 异步等待45s
    async: 45
    # 每5s检查一下
    poll: 5
错误处理
忽略错误

默认情况下,一旦出现错误,后续任务就不在该主机上运行,可以改变此行为:

YAML
1
2
- name: this will not be counted as a failure
  command: /bin/false
修改失败定义 

默认情况下,退出码决定是否失败,你可以修改为基于stderr判断:

YAML
1
2
3
4
- name: this command prints FAILED when it fails
  command: /usr/bin/example-command -x -y -z
  register: command_result
  failed_when: "'FAILED' in command_result.stderr"
标签

前面提到过Tags,它的作用是,仅仅运行Playbook的一部分。 

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 可以对include添加tag
include: foo.yml tags=web,foo
tasks:
    
    - yum: name={{ item }} state=installed
      with_items:
         - httpd
         - memcached
      tags:
         - packages
 
    - template: src=templates/src.j2 dest=/etc/foo.conf
      tags:
         - configuration

Shell
1
2
3
4
5
# 仅仅执行具有某些标签的任务
ansible-playbook example.yml --tags "configuration,packages"
 
# 跳过具有特定标签的任务
ansible-playbook example.yml --skip-tags "notification"
特殊标签

Ansible支持两个特殊标签:

  1. always,具有这种标签的role或play总是会运行,除非明确给出 --skip-tags always
  2. never,具有这种标签的role不会执行,除非明确给出 --tags never
Vault

Ansible 1.5的新版本中,作为Vault Ansible 的一项新功能可将例如passwords、keys等敏感数据文件进行加密,而非存放在明文的 playbooks 或 roles 中。这些 vault 文件可以分散存放也可以集中存放。

命令

创建加密文件:

Shell
1
ansible-vault create foo.yml

你需要指定密码,算法AES。未来使用Vault时需要该密码。包括编辑、查看它时:

Shell
1
2
ansible-vault edit foo.yml
ansible-vault view foo.yml bar.yml baz.yml

可以修改、批量修改多个Vault的密码:

YAML
1
ansible-vault rekey foo.yml bar.yml baz.yml

解密:

Shell
1
ansible-vault decrypt foo.yml bar.yml baz.yml
在剧本中使用 
Shell
1
2
3
4
5
6
#                         询问Vault密码
ansible-playbook site.yml --ask-vault-pass
 
# 密码可以存放在文件或脚本中
ansible-playbook site.yml --vault-password-file ~/.vault_pass.txt
ansible-playbook site.yml --vault-password-file ~/.vault_pass.py
Jinja语法
内置过滤器

过滤器输入显示为第一个参数:

过滤器 说明
abs(number) 取绝对值
attr(obj, name) 获取对象属性
capitalize(s) 转为大写
center(value, width=80) 将字符置于中间

default(value, default=u'', boolean=False)

别名:d

如果变量未定义,则返回指定的默认值:

1
{{ my_variable|default('my_variable is not defined') }}

如果需要当变量可以转换为false时,返回默认值,则第二个参数需要设置为true:

1
{{ ''|default('the string was empty', true) }}
escape(s) 转换为HTML安全的序列 
filesizeformat(value, binary=False) 转换为人类可读的尺寸格式,例如13kb 4.1MB
first(seq) 返回序列的第一个元素
float(value, default=0.0) 转换为浮点数 
format(value, *args, **kwargs)

 C风格的格式化:

1
{{ "%s - %s"|format("Hello?", "Foo!") }} 
indent(s, width=4, indentfirst=False) 缩进文本
int(value, default=0) 转换为整数
join(value, d=u'', attribute=None) 连接列表元素为字符串: {{ [1, 2, 3]|join('|') }}
last(seq) 返回列表的最后一个元素
length(object)

别名:count

返回对象长度

list(value) 转换为列表,如果输入是字符串,则返回字符列表
lower(s) 转换为小写
map()

针对序列的每个对象进行转换操作,可以:

  1. 得到对象的某个属性:
    1
    users|map(attribute='username')|join(', ')
  2. 为每个对象应用过滤器:
    1
    titles|map('lower')|join(', ') 
pprint(value, verbose=False) 漂亮打印,由于调试
random(seq) 随机返回一个条目
reject() 工作方式类似于map,根据属性/过滤器来筛掉某些元素
replace(s, old, new, count=None)

字符串替换:

1
2
{{ "Hello World"|replace("Hello", "Goodbye") }}
    -> Goodbye World
reverse(value) 反转列表或迭代器
round(value, precision=0, method='common') 舍入
select()

工作方式类似于map,根据属性/过滤器来选择某些元素

1
{{ numbers|select("odd") }} 
sort(value, reverse=False, case_sensitive=False, attribute=None)

排序: 

1
2
3
{% for item in iterable|sort %}
    ...
{% endfor %}
string(object) 转换为字符串
striptags(value) 脱去SGML/XML标记,合并多余的空白为单个空格
sum(iterable, attribute=None, start=0) 求和 
trim(value) 去掉首尾空白
truncate(s, length=255, killwords=False, end='...')

截断字符串:

1
2
3
4
{{ "foo bar"|truncate(5) }}
    -> "foo ..."
{{ "foo bar"|truncate(5, True) }}
    -> "foo b..."
upper(s) 转为大写
urlencode(value) 转换为URL中适用的字符串,UTF-8编码
wordcount(s) 统计单词数量
内置测试

对于测试 test(arg,arg1,arg2...),在if语句中需要这样: {% if arg is test(arg1,arg2...) %}

测试 说明
callable(object) 是否可调用
defined(value) 变量是否已定义
divisibleby(value, num) 是否可以整除
escaped(value) 是否已经转义过
even(value) 是否偶数
iterable(value) 是否可迭代
lower(value) 是否小写
mapping(value) 是否是映射,例如dict
none(value) 是否为None
number(value) 是否为数字
odd(value) 是否为奇数
sameas(value, other) 判断是否和另外一个对象具有相同的内存地址
sequence(value) 是否可迭代
string(value) 是否字符串
undefined(value) 是否未定义
upper(value) 是否为大写
全局函数
函数 说明
range([start], stop[, step])

返回一个等差数列的列表:

1
2
3
{% for number in range(10 - users|count) %}
    <li class="empty"><span>...</span></li>
{% endfor %} 
dict(**items) 转换为字典, {'foo' : 'bar'} 与 dict (foo=bar)等价
定界符

要在模板中插入一段表达式,需要使用双括号 {{ expr }}

要在模板中插入一段语句,需要使用 {% statement%}

注释

注释语法为: {#  #}

转义

要转义双括号,可以使用: {{ '{{' }}

对于大段文本,需要保持原样,可以:

1
2
3
4
5
6
7
{% raw %}
    <ul>
    {% for item in seq %}
        <li>{{ item }}</li>
    {% endfor %}
    </ul>
{% endraw %} 
操作符
操作符 说明
+ - * / 加减乘除
// 返回整数商
% 取余数
** N次幂
==   != > >= < <= 比较操作符
and  or not 逻辑运算符
in 元素是否在集合中判断: <span class="pre">{{ 1 in [1,2,3] }}
is 运行测试
| 应用过滤器
~ 将操作数转换为字符串并连接: <span class="pre">{{ "Hello " ~ name ~ "!" }}
.   [] 获取对象属性
() 执行调用
字面值
类型 示例
字符串 "Hello World"
数字 42 / 42.23 分别是整数、浮点数
列表 [ 'list',  'of', 'objects' ]
元组 ( 'tuple', 'of', 'values' )
字典 { 'dict': 'of', 'key': 'and', 'value': 'pairs' }
布尔 true / false
变量
属性访问

支持下标和点号两种导航语法:

1
2
{{ foo.bar }}
{{ foo['bar'] }}
过滤器

可以使用过滤器修改变量,过滤器和变量之间用管道符号 | 分隔,过滤器可以链式调用,前一个的输出,是后一个的输入:

1
{{ name|striptags|title }}

过滤器支持参数,参数需要放在括号中: 

1
{{ list|join(', ') }}
设置变量
1
{% set pipe = joiner("|") %} 
空白控制

可以用 - 符号提示,需要将定界符左侧或右侧的空白移除:

1
2
3
4
5
# 移除右侧
{% for item in seq -%}
    {{ item }}
{%- endfor %}
# 移除左侧
分支判断

 要测试一个变量或表达式,你要在变量后加上一个 is 以及测试的名称:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 测试变量是否定义
{% if name is defined %}
 
# 测试是否可以整除
{% if loop.index is divisibleby 3 %}
{% if loop.index is divisibleby(3) %}
 
 
# if-elif-else
 
{% if kenny.sick %}
    Kenny is sick.
{% elif kenny.dead %}
    You killed Kenny!  You bastard!!!
{% else %}
    Kenny looks okay --- so far
{% endif %}
循环控制
1
2
3
4
5
6
7
8
9
10
# 迭代列表
{% for user in users %}
  <li>{{ user.username|e }}</li>
{% endfor %}
 
# 迭代字典
{% for key, value in my_dict.iteritems() %}
    <dt>{{ key|e }}</dt>
    <dd>{{ value|e }}</dd>
{% endfor %}
特殊变量
变量 描述
loop.index 当前循环迭代的次数(从 1 开始)
loop.index0 当前循环迭代的次数(从 0 开始)
loop.revindex 到循环结束需要迭代的次数(从 1 开始)
loop.revindex0 到循环结束需要迭代的次数(从 0 开始)
loop.first 如果是第一次迭代,为 True 。
loop.last 如果是最后一次迭代,为 True 。
loop.length 序列中的项目数。
loop.cycle 在一串序列间期取值的辅助函数。见下面的解释。
with作用域控制

用于新建一个作用域:

1
2
3
4
5
{% with %}
    {% set foo = 42 %}
    {{ foo }}           foo is 42 here
{% endwith %}
foo is not visible here any longer
常见问题
读取文件内容为变量
YAML
1
2
3
4
5
6
7
- name: Rendering init service job tempalte to local directory
  vars:
    download-script: "{{ lookup('file', 'files/download.sh') }}"
  template:
    src: "init.yaml.j2"
    dest: "{{ role_path }}/files/init-{{item.name}}.yaml"
  with_items: "{{ services }}"

在上面的模板init.yaml.j2中,你可以直接引用变量download-script:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: batch/v1
kind: Job
metadata:
  name: init-{{ item.name }}
spec:
  template:
    spec:
      containers:
      - command:
        - /bin/bash
        - -c
        - |
{{ download_script | indent( width=10, indentfirst=True) }}
模板中如何缩进 

使用indent函数,参考上面。 

← InfluxDB学习笔记
Ceph学习笔记 →

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code class="" title="" data-url=""> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre class="" title="" data-url=""> <span class="" title="" data-url="">

Related Posts

  • IPVS和Keepalived
  • Linux的三种Init机制
  • Ubuntu下安装subversion服务器
  • Linux内核学习笔记(五)
  • Linux编程知识集锦

Recent Posts

  • Investigating and Solving the Issue of Failed Certificate Request with ZeroSSL and Cert-Manager
  • A Comprehensive Study of Kotlin for Java Developers
  • 背诵营笔记
  • 利用LangChain和语言模型交互
  • 享学营笔记
ABOUT ME

汪震 | Alex Wong

江苏淮安人,现居北京。目前供职于腾讯云,专注容器方向。

GitHub:gmemcc

Git:git.gmem.cc

Email:gmemjunk@gmem.cc@me.com

ABOUT GMEM

绿色记忆是我的个人网站,域名gmem.cc中G是Green的简写,MEM是Memory的简写,CC则是我的小天使彩彩名字的简写。

我在这里记录自己的工作与生活,同时和大家分享一些编程方面的知识。

GMEM HISTORY
v2.00:微风
v1.03:单车旅行
v1.02:夏日版
v1.01:未完成
v0.10:彩虹天堂
v0.01:阳光海岸
MIRROR INFO
Meta
  • Log in
  • Entries RSS
  • Comments RSS
  • WordPress.org
Recent Posts
  • Investigating and Solving the Issue of Failed Certificate Request with ZeroSSL and Cert-Manager
    In this blog post, I will walk ...
  • A Comprehensive Study of Kotlin for Java Developers
    Introduction Purpose of the Study Understanding the Mo ...
  • 背诵营笔记
    Day 1 Find Your Greatness 原文 Greatness. It’s just ...
  • 利用LangChain和语言模型交互
    LangChain是什么 从名字上可以看出来,LangChain可以用来构建自然语言处理能力的链条。它是一个库 ...
  • 享学营笔记
    Unit 1 At home Lesson 1 In the ...
  • K8S集群跨云迁移
    要将K8S集群从一个云服务商迁移到另外一个,需要解决以下问题: 各种K8S资源的迁移 工作负载所挂载的数 ...
  • Terraform快速参考
    简介 Terraform用于实现基础设施即代码(infrastructure as code)—— 通过代码( ...
  • 草缸2021
    经过四个多月的努力,我的小小荷兰景到达极致了状态。

  • 编写Kubernetes风格的APIServer
    背景 前段时间接到一个需求做一个工具,工具将在K8S中运行。需求很适合用控制器模式实现,很自然的就基于kube ...
  • 记录一次KeyDB缓慢的定位过程
    环境说明 运行环境 这个问题出现在一套搭建在虚拟机上的Kubernetes 1.18集群上。集群有三个节点: ...
  • eBPF学习笔记
    简介 BPF,即Berkeley Packet Filter,是一个古老的网络封包过滤机制。它允许从用户空间注 ...
  • IPVS模式下ClusterIP泄露宿主机端口的问题
    问题 在一个启用了IPVS模式kube-proxy的K8S集群中,运行着一个Docker Registry服务 ...
  • 念爷爷
      今天是爷爷的头七,十二月七日、阴历十月廿三中午,老人家与世长辞。   九月初,回家看望刚动完手术的爸爸,发

  • 6 杨梅坑

  • liuhuashan
    深圳人才公园的网红景点 —— 流花山

  • 1 2020年10月拈花湾

  • 内核缺陷触发的NodePort服务63秒延迟问题
    现象 我们有一个新创建的TKE 1.3.0集群,使用基于Galaxy + Flannel(VXLAN模式)的容 ...
  • Galaxy学习笔记
    简介 Galaxy是TKEStack的一个网络组件,支持为TKE集群提供Overlay/Underlay容器网 ...
TOPLINKS
  • Zitahli's blue 91 people like this
  • 梦中的婚礼 64 people like this
  • 汪静好 61 people like this
  • 那年我一岁 36 people like this
  • 为了爱 28 people like this
  • 小绿彩 26 people like this
  • 彩虹姐姐的笑脸 24 people like this
  • 杨梅坑 6 people like this
  • 亚龙湾之旅 1 people like this
  • 汪昌博 people like this
  • 2013年11月香山 10 people like this
  • 2013年7月秦皇岛 6 people like this
  • 2013年6月蓟县盘山 5 people like this
  • 2013年2月梅花山 2 people like this
  • 2013年淮阴自贡迎春灯会 3 people like this
  • 2012年镇江金山游 1 people like this
  • 2012年徽杭古道 9 people like this
  • 2011年清明节后扬州行 1 people like this
  • 2008年十一云龙公园 5 people like this
  • 2008年之秋忆 7 people like this
  • 老照片 13 people like this
  • 火一样的六月 16 people like this
  • 发黄的相片 3 people like this
  • Cesium学习笔记 90 people like this
  • IntelliJ IDEA知识集锦 59 people like this
  • Bazel学习笔记 38 people like this
  • 基于Kurento搭建WebRTC服务器 38 people like this
  • PhoneGap学习笔记 32 people like this
  • NaCl学习笔记 32 people like this
  • 使用Oracle Java Mission Control监控JVM运行状态 29 people like this
  • Ceph学习笔记 27 people like this
  • 基于Calico的CNI 27 people like this
Tag Cloud
ActiveMQ AspectJ CDT Ceph Chrome CNI Command Cordova Coroutine CXF Cygwin DNS Docker eBPF Eclipse ExtJS F7 FAQ Groovy Hibernate HTTP IntelliJ IO编程 IPVS JacksonJSON JMS JSON JVM K8S kernel LB libvirt Linux知识 Linux编程 LOG Maven MinGW Mock Monitoring Multimedia MVC MySQL netfs Netty Nginx NIO Node.js NoSQL Oracle PDT PHP Redis RPC Scheduler ServiceMesh SNMP Spring SSL svn Tomcat TSDB Ubuntu WebGL WebRTC WebService WebSocket wxWidgets XDebug XML XPath XRM ZooKeeper 亚龙湾 单元测试 学习笔记 实时处理 并发编程 彩姐 性能剖析 性能调优 文本处理 新特性 架构模式 系统编程 网络编程 视频监控 设计模式 远程调试 配置文件 齐塔莉
Recent Comments
  • qg on Istio中的透明代理问题
  • heao on 基于本地gRPC的Go插件系统
  • 黄豆豆 on Ginkgo学习笔记
  • cloud on OpenStack学习笔记
  • 5dragoncon on Cilium学习笔记
  • Archeb on 重温iptables
  • C/C++编程:WebSocketpp(Linux + Clion + boostAsio) – 源码巴士 on 基于C/C++的WebSocket库
  • jerbin on eBPF学习笔记
  • point on Istio中的透明代理问题
  • G on Istio中的透明代理问题
  • 绿色记忆:Go语言单元测试和仿冒 on Ginkgo学习笔记
  • point on Istio中的透明代理问题
  • 【Maven】maven插件开发实战 – IT汇 on Maven插件开发
  • chenlx on eBPF学习笔记
  • Alex on eBPF学习笔记
  • CFC4N on eBPF学习笔记
  • 李运田 on 念爷爷
  • yongman on 记录一次KeyDB缓慢的定位过程
  • Alex on Istio中的透明代理问题
  • will on Istio中的透明代理问题
  • will on Istio中的透明代理问题
  • haolipeng on 基于本地gRPC的Go插件系统
  • 吴杰 on 基于C/C++的WebSocket库
©2005-2025 Gmem.cc | Powered by WordPress | 京ICP备18007345号-2