Ansible基礎
Ansible
官網文檔:
安裝
yum安裝
配置yum源
$ cat /etc/yum.repos.d/ansible.repo
[epel]
name=epel
baseurl=//mirrors.aliyun.com/epel/7Server/x86_64/
enable=1
gpgcheck=0
安裝ansible
$ yum -y install ansible
如果有pip工具,也可以運行pip install ansible下載。使用pip下載的ansible不提供默認配置文件/etc/ansible/ansible.cfg。
安裝完成檢查
$ ansible --version
ansible 2.9.25
config file = /etc/ansible/ansible.cfg
configured module search path = [u'/home/test/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
ansible python module location = /usr/lib/python2.7/site-packages/ansible
executable location = /bin/ansible
python version = 2.7.5 (default, Oct 14 2020, 14:45:30) [GCC 4.8.5 20150623 (Red Hat 4.8.5-44)]
Ansible參數補全功能(可選)
從Ansible 2.9版本開始,ansible支持命令的選項補全功能,它依賴於python的argcomplete插件
$ yum -y install python-argcomplete # 安裝
$ activate-global-python-argcomplete # 激活
激活後,就可以按兩次tab補全命令和選項了。
環境配置
配置主機名解析
在master節點的/etc/hosts文件中,添加上ip主機名對應關係
$ cat /etc/hosts
127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4
::1 localhost localhost.localdomain localhost6 localhost6.localdomain6
192.168.175.100 master
192.168.175.101 node1
192.168.175.102 node2
配置主機互信
ansible默認採用ssh連接,ssh連接要麼使用密碼認證,要麼使用公鑰認證。
這裡採用公鑰認證,實現免密登錄,後續使用ansible方便一些。
$ ssh-keygen -t rsa -f ~/.ssh/id_rsa -N ''
$ ssh-keyscan 192.168.175.101 >> ~/.ssh/known_hosts 2>/dev/null
$ ssh-keyscan 192.168.175.102 >> ~/.ssh/known_hosts 2>/dev/null
$ sshpass -p'hello123' ssh-copy-id [email protected]
測試:
[root@master ~]# ssh 192.168.175.101
Last login: Wed Jan 5 12:24:18 2022 from 192.168.175.1
[root@node1 ~]# exit
logout
Connection to 192.168.175.101 closed.
[root@master ~]#
發現不用輸入密碼即可直接登錄其它主機。
安裝、環境配置完成後,ansible還只能控制本機,不能實現對其它節點的批量控制。
若想實現它的真正功能,必須對它進行配置。
下面開始了解ansible的幾個重要的配置文件。
ansible的配置文件
全局配置文件
ansible默認的全局配置文件是/etc/ansible/ansible.cfg
,可認為是全局配置的入口。
Ansible支持4種方式指定配置文件,它們的解析順序從上到下
- ANSIBLE_CFG 環境變量指定的配置文件
- ansible.cfg 當前目錄下的ansible.cfg
- ~/.ansible.cfg 家目錄下的.ansible.cfg
/etc/ansible/ansible.cfg
默認全局配置文件
修改默認配置文件/etc/ansible/ansible.cfg:
$ cat /etc/ansible/ansible.cfg | grep -vE "^$|^#"
[defaults]
inventory = /etc/ansible/hosts # inventory文件:ansible管理的主機清單
library = /usr/share/my_modules/
forks = 5 # ansbile的並發連接數
sudo_user = root
remote_port = 22
host_key_checking = False
timeout = 10
log_path = /var/log/ansible.log
文件中其它的可配置項:
stdout_callback = debug # 可將輸出變得人性化;默認輸出會擠在一行,配置後會換行輸出;
inventory主機文件
inventory
文件默認路徑是/etc/ansible/hosts
,在這裡配置目標主機,ansible便可以對其進行控制。
在/etc/ansible/hosts
文件里配置node主機名或ip:
$ cat hosts
[default] # 主機分組
node1 # 192.168.175.101
node2 # 192.168.175.102
可以通過/etc/ansible/ansible.cfg
文件修改inventory的默認路徑:inventory = /etc/ansible/hosts
。如果將該配置指定為目錄,便可以使用多個inventory文件來管理節點,一般很少動這個。
通常,不會修改默認的路徑。如果有自定義的inventory文件,可以直接在ansible命令行中使用-i
選項指定:
# ansible -i /tmp/my_inventory.ini ...
# ansible-playbook -i /tmp/my_inventory.ini ...
查看inventory
列出ansiblek可管理的所有主機
$ ansible-inventory --graph all # 指定文件:ansible-inventory -i /etc/ansible/hosts --graph all
@all: # all是默認的主機組,包含所有主機
|--@default:
| |--node1
| |--node2
|--@ungrouped:
運行ansible命令
完成上述的基本配置後,即可以開始使用ansible來批量管理主機了,這裡的管理方式為命令行方式(又稱為Ad-hoc
方式)。
Ad-hoc方式運行ansible的命令格式:ansible 主機組/主機 -m 模塊 -a 參數
選項解析:
-m: 指定調用的模塊
-a: 向模塊傳遞的參數,模塊不需要則可省略;參數需要使用引號包圍
此外ansible命令還可以帶上其它選項:
-i: 指定本次的inventory路徑,指定該參數則後面不加主機組/主機
-e: 設置變量,格式為'var1="aaa" var="bbb"'
-v/vv/vvv: 命令輸出的打印級別
ansible的批量管理功能依靠各個模塊來完成,ansible提供了幾千個模塊(其中ansible團隊自己維護大約100多個核心模塊),每個模塊完成各自的作用。
下面用ping模塊和debug模塊來演示一下ansible的基礎功能。
ping模塊
ping模塊
是ansible最基礎模塊之一,可用於檢測遠程主機是否在線。
命令:ansible 主機組/主機 -m ping
返回值:changed、ping
命令:ansible all -m ping
[root@master ansible]$ ansible all -m ping
node2 | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python"
},
"changed": false,
"ping": "pong"
}
node1 | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python"
},
"changed": false,
"ping": "pong"
}
debug模塊
官網說明://docs.ansible.com/ansible/latest/collections/ansible/builtin/debug_module.html
debug模塊
用於輸出或調試一些變量或數據。該模塊共有三個參數msg、var、verbosity。
msg:打印配置的信息
var:打印變量值
verbosity:運行級別,設置為3,則-vvv或更高才會打印輸出
命令:
ansible all -m debug -a 'msg="hello world"'
ansible all -e 'str="hello world"' -m debug -a 'var=str'
ansible all -v -m debug -a 'msg="hello world" verbosity=1'
示例:
[root@master ansible]$ ansible all -m debug -a 'msg="hello world"'
node1 | SUCCESS => {
"msg": "hello world"
}
node2 | SUCCESS => {
"msg": "hello world"
}
[root@master ansible]$ ansible all -e 'str="hello world"' -m debug -a 'var=str'
node1 | SUCCESS => {
"str": "hello world"
}
node2 | SUCCESS => {
"str": "hello world"
}
[root@master ansible]$ ansible all -m debug -a 'msg="hello world" verbosity=1'
node1 | SKIPPED
node2 | SKIPPED
[root@master ansible]$ ansible all -v -m debug -a 'msg="hello world" verbosity=1'
Using /etc/ansible/ansible.cfg as config file
node1 | SUCCESS => {
"msg": "hello world"
}
node2 | SUCCESS => {
"msg": "hello world"
}
playbook
playbook、play、task的關係
- playbook中可以定義一個或多個play
- 每個play中可以定義一個或多個task
- 其中還可以定義兩類特殊的task:pre_tasks和post_tasks
- pre_tasks表示執行執行普通任務之前執行的任務列表
- post_tasks表示普通任務執行完之後執行的任務列表
- 每個play都需要通過hosts指令指定要執行該play的目標主機
- 每個play都可以設置一些該play的環境控制行為,比如定義play級別的變量
編寫一個playbook並執行
playbook
使用yaml
語法格式組織各種play
和task
規則。
下面使用ping模塊和debug模塊編寫一個playbook文件如下:
# cat test.yaml
---
- name: play1 # play的名稱,非必須
hosts: all # 指定目標主機
gather_facts: false # 收集目標主機信息,默認值true,非必須
tasks: # tasks聲明任務列表
- name: task1 # task任務名稱,非必須
ping: # 模塊
data: "pong task1" # 模塊參數
- name: task2
ping:
data: "pong task2"
- name: play2
hosts: all
gather_facts: false
tasks:
- name: task1
debug:
msg: "hello task1 in play2"
- name: task2
debug:
msg: "hello task2 in play2"
注意所有的
-
和:
符號後面均需要接一個空格
執行playbook的命令是ansible-playbook test.yaml
:
該命令同樣支持像ansile命令一樣的多個選項,如-e
、-i
、-v
等
$ ansible-playbook -v test.yaml
Using /etc/ansible/ansible.cfg as config file
PLAY [play1] **********************************************************************************************************************
TASK [task1] **********************************************************************************************************************
ok: [node2] => {"ansible_facts": {"discovered_interpreter_python": "/usr/bin/python"}, "changed": false, "ping": "pong task1"}
ok: [node1] => {"ansible_facts": {"discovered_interpreter_python": "/usr/bin/python"}, "changed": false, "ping": "pong task1"}
TASK [task2] **********************************************************************************************************************
ok: [node1] => {"changed": false, "ping": "pong task2"}
ok: [node2] => {"changed": false, "ping": "pong task2"}
PLAY [play2] **********************************************************************************************************************
TASK [task1] **********************************************************************************************************************
ok: [node1] => {
"msg": "hello task1 in play2"
}
ok: [node2] => {
"msg": "hello task1 in play2"
}
TASK [task2] **********************************************************************************************************************
ok: [node1] => {
"msg": "hello task2 in play2"
}
ok: [node2] => {
"msg": "hello task2 in play2"
}
PLAY RECAP ************************************************************************************************************************
node1 : ok=4 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
node2 : ok=4 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
playbook文件各配置指令及含義
yaml
文件中,使用-
表示一個列表元素,多個key:value
表示一個字典。
每個playbook
使用列表來組織多個play
,play
內同樣使用列表來組織多個task
;play和task自身則採用字典的方式組織,即多個鍵值對。
在playbook頂層使用- xxx:
表示這是一個play;每個play中必須包含hosts
和tasks
指令。
hosts
指令用來指定要執行該play的目標主機,可以是主機名、主機組或者其它多種方式。
tasks
指令用來指定該play中包含的任務列表,每個任務使用- xxx:
方式表示。
name
指令用來設置play和task的名稱,值具有唯一性。
gather_facts
指令用來收集目標主機的信息,由setup模塊提供。默認情況下,每個play都先執行這個特殊任務,收集完信息才開始其它任務。如果後續任務中用不到該信息,則可以禁止掉該任務,提升效率。
向模塊傳遞參數
yaml中,向模塊傳遞參數的方式總結為字符串和數組兩種方式。
還是以debug模塊為例。
數組方式如上面test.yaml文件里的傳參,即key: value
的形式:
......
- name: task1
debug:
msg: "hello task1 in play2"
verbosity=1
字符串方式,由於yaml的語法規則(字符串換行將自動轉換為空格),又有不同的書寫形式:
---
- name: debug
hosts: all
gather_facts: false
tasks:
- name: task1
debug:
msg="hello task1" verbosity=1 # 參數寫成一行
- name: task2
debug:
msg="hello task2" # 參數寫成多行
verbosity=1
- name: task3
debug: | # 豎線|,將保留字符串的換行符,否則將自動轉換成空格,在一些模塊中很有用,如shell
msg="hello task3"
verbosity=1
- name: task4
debug: > # 符號>,效果和直接寫成多行一樣
msg="hello task4"
verbosity=1
還可以直接使用指令args指明參數:
......
- name: task1
debug:
args:
msg: "hello task1 in play2"
默認的任務執行策略
/etc/ansible/ansible.cfg文件的forks
配置,決定ansible執行任務的並發連接數。
假如forks配置為5,那麼ansible第一次將同時連接5個node節點執行任務。其中若有節點提前執行完任務, 則ansible會新建一個新進程,來連接下一個節點執行任務。
forks是保證最多有N個節點同時執行任務。
常見模塊
shell模塊
說明:
shell
模塊接收shell命令,命令後可跟空格分割的參數列;
必須傳入自由格式的命令,或者cmd參數;
它十分類似command模塊,但是它在遠程主機上通過shell(比如/bin/bash)來運行命令;
shell模塊相比command模塊,支持解析特殊符號<
、>
、|
、;
、&
等。
參數:
Parameter | Choices/Defaults | 說明 |
---|---|---|
chdir path |
運行腳本前,切換到相關目錄 | |
cmd string |
需要運行的命令,後跟可選的參數 | |
creates path |
若一個文件或目錄存在,則跳過該步驟 | |
removes path |
若一個文件或目錄不存在,則跳過該步驟 | |
executable path |
改變執行命令的解釋器,如/bin/bash、/usr/bin/expect、/usr/bin/python;絕對路徑 | |
free_form string | 自由格式的命令(即命令字符串,沒有相關參數,直接寫在shell模塊後面即可) | |
stdin string | Set the stdin of the command directly to the specified value. | |
stdin_add_newline boolean | Choices:noyes ← | Whether to append a newline to stdin data. |
warn boolean | Choices:noyes ← | Whether to enable task warnings. |
示例:
---
- name: shell
hosts: all
gather_facts: no
tasks:
- name: task1
shell:
hostname
- name: task2
shell:
cmd: date +"%F %T"
- name: task3
shell:
cmd: pwd
chdir: /etc/sysconfig
- name: task4
shell:
cmd: ls /tmp
creates: /tmp
- name: task5
shell:
cmd: print('hello world')
executable: /usr/bin/python
注意chdir參數只支持下面的方式:
- name: task3
shell:
cmd: pwd
chdir: /etc/sysconfig
- name: task3
shell: pwd # free_from格式需要採用args參數,顯式指定chdir參數
args:
chdir: /etc/sysconfig
# 下面的方式將發生錯誤
- name: task3
shell: pwd
chdir: /etc/sysconfig
Ad-hoc方式:
$ ansible all -m shell -a "ls -l | wc -l"
$ ansible all -v -m shell -a "ls chdir=/tmp "
script模塊
說明:
script
模塊接受一個腳本名稱,後面可跟空格分割的參數列;
支持自由格式的命令,或者cmd參數;
將本地腳本傳輸到遠程主機上執行;
在遠程主機上使用shell環境執行腳本;
該模塊不需要python,類似raw模塊。
參數:
Parameter | Choices/Defaults | 說明 |
---|---|---|
chdir path |
運行腳本前,切換到遠程主機的相關目錄 | |
cmd string |
需要運行的腳本路徑,後跟可選的參數 | |
creates path |
若一個文件或目錄存在,則跳過該步驟 | |
removes path |
若一個文件或目錄不存在,則跳過該步驟 |
示例:
- name: Run a script with arguments (free form)
ansible.builtin.script: /some/local/script.sh --some-argument 1234
- name: Run a script with arguments (using 'cmd' parameter)
ansible.builtin.script:
cmd: /some/local/script.sh --some-argument 1234
- name: Run a script only if file.txt does not exist on the remote node
ansible.builtin.script: /some/local/create_file.sh --some-argument 1234
args:
creates: /the/created/file.txt
- name: Run a script only if file.txt exists on the remote node
ansible.builtin.script: /some/local/remove_file.sh --some-argument 1234
args:
removes: /the/removed/file.txt
- name: Run a script using an executable in a non-system path
ansible.builtin.script: /some/local/script
args:
executable: /some/remote/executable
- name: Run a script using an executable in a system path
ansible.builtin.script: /some/local/script.py
args:
executable: python3
Ad-hoc:
$ ansible all -m script -a "/tmp/hello.sh world"
$ ansible all -m script -a "/tmp/hello.sh world creates=/tmp"
hostname模塊
說明:
設置遠程設備的主機名。
//docs.ansible.com/ansible/latest/collections/ansible/builtin/hostname_module.html
在playbook中設置變量
vars
指令可在play或task中設置變量,可以設置一個或多個。可以採用字典或列表的形式定義變量。
字典變量的定義和引用
定義:
vars:
foo1:
a: hello
b: world
foo2:
a: aaa
b: bbb
引用:
使用點號或方括號,在yaml文件使用jinja2語法引用,需要加單雙引號,否則解析yaml的時候將報錯
- name: task1
debug:
msg: "{{ foo1.a }} {{ foo1['b'] }}" # 注意{{}}是jinja2的語法,在yaml文件中需要使用引號引起來,單雙引號都行
- name: task2
debug:
var: foo1.a,foo1['b'] # debug模塊的var參數,多個值使用逗號分隔,且無需花括號,前後加不加引號均可
- name: task3
debug:
msg: '{{ foo2.a }} {{ foo2.b }}'
示例:
---
- name: vars play
hosts: all
gather_facts: no
vars:
foo1:
a: hello
b: world
foo2:
a: aaa
b: bbb
tasks:
- name: task1
debug:
msg: "{{ foo1.a }} {{ foo1['b'] }}"
- name: task2
debug:
var: foo1.a,foo1['b']
- name: task3
debug:
msg: '{{ foo2.a }} {{ foo2.b }}'
列表變量的定義和引用
定義:
vars:
foo:
- a: hello
b: world
- a: aaa
b: bbb
引用:
- name: task1
debug:
msg: "{{ foo[0].a }} {{ foo[0].b }}"
- name: task2
debug:
msg: "{{foo[1]['a']}} {{foo[1]['b']}}"
引用變量時,使用點號比較方便,但如果變量名本身帶點,則盡量選擇方括號的方式。
when指令進行條件判斷
when
指令是ansible提供的唯一一個通用條件指令。when
指令後的變量引用不需要雙花括號,當when指令的值為true時,執行任務。
yaml文件如下:
---
- name: when
hosts: all
gather_facts: no
vars:
foo: test
tasks:
- name: task1
when: foo == "test" # when指令的變量可直接引用
debug:
msg: "hello"
- name: task2
when: foo == "dev"
debug:
msg: "world"
示例:
從輸出中可看出,任務task2由於條件不滿足自動跳過
[root@master ansible]# ansible-playbook -v when.yaml
Using /etc/ansible/ansible.cfg as config file
PLAY [when] ***********************************************************************************************************************
TASK [task1] **********************************************************************************************************************
ok: [node1] => {
"msg": "hello"
}
ok: [node2] => {
"msg": "hello"
}
TASK [task2] **********************************************************************************************************************
skipping: [node1] => {}
skipping: [node2] => {}
PLAY RECAP ************************************************************************************************************************
node1 : ok=1 changed=0 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
node2 : ok=1 changed=0 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
多個判斷條件:
- name: task3
when: foo == "dev" or foo == "test" # 邏輯或
debug:
msg: "hello world"
- name: task4
when: (foo == "dev") or (foo == "test") # 支持括號括起來
debug:
msg: "hello world"
- name: task5
when: (foo == "dev") and (foo == "test") # 邏輯與
debug:
msg: "hello world"
- name: task6
when:
- foo == "dev" # 邏輯與的另一種組織方式
- foo == "test"
debug:
msg: "hello world"
- name: task7
when: foo != "dev" # 邏輯否
debug:
msg: "hello world"
loop循環結構
loop指令中的各項元素將以item變量名進行迭代。
直接迭代列表
迭代簡單列表:
列表元素為字符串
- name: task1
shell: "{{ item }}"
loop:
- hostname
- "uptime -p"
示例:
[root@master ansible]# cat loop.yaml
---
- name: loop
hosts: all
gather_facts: no
tasks:
- name: task1
shell: "{{ item }}"
loop:
- hostname
- "uptime -p"
[root@master ansible]# ansible-playbook -v loop.yaml
Using /etc/ansible/ansible.cfg as config file
PLAY [loop] ***********************************************************************************************************************
TASK [task1] **********************************************************************************************************************
changed: [node2] => (item=hostname) => {"ansible_facts": {"discovered_interpreter_python": "/usr/bin/python"}, "ansible_loop_var": "item", "changed": true, "cmd": "hostname", "delta": "0:00:00.020436", "end": "2022-01-09 15:05:17.126971", "item": "hostname", "rc": 0, "start": "2022-01-09 15:05:17.106535", "stderr": "", "stderr_lines": [], "stdout": "node2", "stdout_lines": ["node2"]}
changed: [node1] => (item=hostname) => {"ansible_facts": {"discovered_interpreter_python": "/usr/bin/python"}, "ansible_loop_var": "item", "changed": true, "cmd": "hostname", "delta": "0:00:00.361005", "end": "2022-01-09 15:05:17.409020", "item": "hostname", "rc": 0, "start": "2022-01-09 15:05:17.048015", "stderr": "", "stderr_lines": [], "stdout": "node1", "stdout_lines": ["node1"]}
changed: [node2] => (item=uptime -p) => {"ansible_loop_var": "item", "changed": true, "cmd": "uptime -p", "delta": "0:00:00.006628", "end": "2022-01-09 15:05:17.610052", "item": "uptime -p", "rc": 0, "start": "2022-01-09 15:05:17.603424", "stderr": "", "stderr_lines": [], "stdout": "up 3 weeks, 2 days, 19 hours, 42 minutes", "stdout_lines": ["up 3 weeks, 2 days, 19 hours, 42 minutes"]}
changed: [node1] => (item=uptime -p) => {"ansible_loop_var": "item", "changed": true, "cmd": "uptime -p", "delta": "0:00:00.074039", "end": "2022-01-09 15:05:18.006283", "item": "uptime -p", "rc": 0, "start": "2022-01-09 15:05:17.932244", "stderr": "", "stderr_lines": [], "stdout": "up 3 weeks, 2 days, 19 hours, 42 minutes", "stdout_lines": ["up 3 weeks, 2 days, 19 hours, 42 minutes"]}
PLAY RECAP ************************************************************************************************************************
node1 : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
node2 : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
迭代hash哈希列表:
列表元素為字典
- name: task1
shell:
cmd: "{{ item.cmd }}"
creates: "{{ item['condition'] }}" # 兩種引用方式均可
loop:
- { cmd: 'hostname', condition: '/tmp' } # 這個循環將被跳過
- { cmd: 'uptime', condition: '/aaa' }
示例:
[root@master ansible]# cat loop.yaml
---
- name: loop
hosts: all
gather_facts: no
tasks:
- name: task1
shell:
cmd: "{{ item.cmd }}"
creates: "{{ item.condition }}"
loop:
- { cmd: 'hostname', condition: '/tmp' } # 這個循環將被跳過,["skipped, since /tmp exists"],任務沒有跳過
- { cmd: 'uptime', condition: '/aaa' }
[root@master ansible]#
[root@master ansible]# ansible-playbook -v loop.yaml
Using /etc/ansible/ansible.cfg as config file
PLAY [loop] ***********************************************************************************************************************
TASK [task1] **********************************************************************************************************************
ok: [node2] => (item={u'cmd': u'hostname', u'condition': u'/tmp'}) => {"ansible_facts": {"discovered_interpreter_python": "/usr/bin/python"}, "ansible_loop_var": "item", "changed": false, "cmd": "hostname", "item": {"cmd": "hostname", "condition": "/tmp"}, "rc": 0, "stdout": "skipped, since /tmp exists", "stdout_lines": ["skipped, since /tmp exists"]}
changed: [node2] => (item={u'cmd': u'uptime', u'condition': u'/aaa'}) => {"ansible_loop_var": "item", "changed": true, "cmd": "uptime", "delta": "0:00:00.024675", "end": "2022-01-09 15:21:23.286758", "item": {"cmd": "uptime", "condition": "/aaa"}, "rc": 0, "start": "2022-01-09 15:21:23.262083", "stderr": "", "stderr_lines": [], "stdout": " 15:21:23 up 23 days, 19:58, 2 users, load average: 0.13, 0.14, 0.20", "stdout_lines": [" 15:21:23 up 23 days, 19:58, 2 users, load average: 0.13, 0.14, 0.20"]}
ok: [node1] => (item={u'cmd': u'hostname', u'condition': u'/tmp'}) => {"ansible_facts": {"discovered_interpreter_python": "/usr/bin/python"}, "ansible_loop_var": "item", "changed": false, "cmd": "hostname", "item": {"cmd": "hostname", "condition": "/tmp"}, "rc": 0, "stdout": "skipped, since /tmp exists", "stdout_lines": ["skipped, since /tmp exists"]}
changed: [node1] => (item={u'cmd': u'uptime', u'condition': u'/aaa'}) => {"ansible_loop_var": "item", "changed": true, "cmd": "uptime", "delta": "0:00:00.386491", "end": "2022-01-09 15:21:30.029184", "item": {"cmd": "uptime", "condition": "/aaa"}, "rc": 0, "start": "2022-01-09 15:21:29.642693", "stderr": "", "stderr_lines": [], "stdout": " 15:21:30 up 23 days, 19:58, 2 users, load average: 0.90, 0.51, 0.35", "stdout_lines": [" 15:21:30 up 23 days, 19:58, 2 users, load average: 0.90, 0.51, 0.35"]}
PLAY RECAP ************************************************************************************************************************
node1 : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
node2 : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
迭代字典
loop
指令無法直接迭代字典,需要使用過濾器dict2items
進行轉換,如下:
- name: Using dict2items
ansible.builtin.debug:
msg: "{{ item.key }} - {{ item.value }}"
loop: "{{ tag_data | dict2items }}"
vars:
tag_data:
Environment: dev
Application: payment
上例中,通過迭代tag_data來打印它的key和value。
when
指令和loop
指令同時使用時,先進行循環,再在每個循環中進行條件判斷。
notify和handlers
ansible中的一個重要概念changed
,它表示目標狀態是否發生改變,即本次任務是否執行、執行後是否影響結果。如果changed=1,則表示目標狀態發生改變;如果changed=0,則表示目標狀態未發生改變,或者任務沒有執行。
ansible若監視到changed=1
,就會觸發notify
指令所定義的handler
。handler,也是一個task,只是定義在handlers
中,需要notify來觸發執行。
handlers的使用與tasks使用一樣,notify和hanlders中任務名稱必須一樣。
當一個play中所有任務都執行完成後,handler才會執行。好處是可以多次觸發notify,但最後只執行一次對應的handler。
示例:
---
- name: notify and handlers
hosts: all
gather_facts: no
tasks:
- name: task1
shell: uptime
notify: hello
handlers:
- name: hello
debug:
msg: "hello world"
執行結果:
[root@master ansible]# ansible-playbook notify.yaml
PLAY [notify and handlers] ********************************************************************************************************
TASK [task1] **********************************************************************************************************************
changed: [node2]
changed: [node1]
RUNNING HANDLER [hello] ***********************************************************************************************************
ok: [node2] => {}
MSG:
hello world
ok: [node1] => {}
MSG:
hello world
PLAY RECAP ************************************************************************************************************************
node1 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
node2 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
組織playbook
將所有play全部寫在一個yaml文件中,固然可行,但是可讀性、維護性太差。
比較好的做法是,將同類任務的play放在一個文件中。多個任務,則寫成多個文件,最後使用一個入口文件來引用這些任務文件。
假設入口文件名為main.yaml,在該文件中使用import_playbook
指令引用其它playbook:
---
- import_playbook: "init_server/aaa.yaml"
- import_playbook: "init_server/bbb.yaml"
- import_playbook: "init_server/ccc.yaml"
- import_playbook: "init_server/ddd.yaml"
執行方式不變:ansible-playbook main.yaml
組織各類內容:task、handler、變量
上面介紹了使用playbook指令來引入多個playbook文件,從而提高可讀性和維護性。
其實,ansible還提供了更加規範的方式,來組織更多的內容,即
role
和colllection
,collection這裡暫不涉及。
ansible可組織的內容包括:
playbook
task
variable
handler
(handler也是一個task,只是編寫在handlers內部)role
可組織的意思是說,可將相同內容定義在同一個文件中,然後使用相關指令來引入指定文件內容。
- 引入
palybook
:import_playbook
- 引入
task
或handler
:import_tasks
、include_tasks
- 引入
variable
:vars_files
、include_vars
- 引入
role
:import_role
、include_role
、roles
import和include兩種引入方式有所區別,前者為靜態加載
(在playbook解析的階段,內容將寫入到引入的位置),後者為動態加載
(解析階段不引入,而是在執行階段才引入)。
組織tasks
可將task單獨寫在一個文件中,然後在play里使用import_tasks
、include_tasks
模塊引入。
示例:
編寫一個tasks文件tasks.yaml:
---
- name: task1
debug:
msg: "hello"
- name: task2
debug:
msg: "world"
在playbook中引入:
---
- name: play1
hosts: node1
gather_facts: no
tasks:
- name: task1 & task2
import_tasks: tasks.yaml # include_tasks模塊也行
在循環中引入tasks文件,必須使用include_tasks指令
---
- name: play1
hosts: node1
gather_facts: no
tasks:
- name: loop tasks
include_tasks: tasks.yaml
loop:
- one
- two
組織handlers
一般情況下playbook的handlers如下,在task中使用handler的任務名來觸發
---
- name: play1
hosts: node1
gather_facts: no
tasks:
- name: task1
shell: uptime
notify: h1
- name: task2
shell: date
notify: h2
handlers:
- name: h1
debug:
msg: "run h1"
- name: h2
debug:
msg: "run h2"
可將handler單獨編寫在一個文件中,如下:
$ cat handler1.yaml
---
- name: handler1
debug:
msg: "run handler1"
$ cat handler2.yaml
---
- name: handler2
debug:
msg: "run handler2"
然後在playbook中使用import_tasks
或include_tasks
指令來引入,同時在notify
中修改對應的handler
名:
---
- name: play1
hosts: node1
gather_facts: no
tasks:
- name: task1
shell: uptime
notify: handler1 # handler1為靜態引入,notify使用handler名
- name: task2
shell: date
notify: h2 # handler2為動態引入,notify使用引入的任務名
handlers:
- name: h1
import_tasks: handler1.yaml # import_tasks靜態引入
- name: h2
include_tasks: handler2.yaml # include_tasks動態引入
組織變量
前面已經介紹了如何在playbook中使用var
指令直接設置變量,除了這個方法,ansible還支持將變量單獨放在一個文件中,然後在play中使用vars_files
指令或include_vars
模塊來引入該變量文件。也可以在命令行中,使用-e
選項(–extra_vars)來設置變量或引入變量文件。
vars_files
是play
級別的指令,用於play中,在playbook解析階段引入變量文件;
include_vars
是任務模塊(類似模塊一樣),用在tasks中定義一個引入變量的任務,只有該任務執行之後,才會創建變量;
-e
選項在命令行中,全局有效;
vars_files示例
變量文件varfile.yaml,變量的定義一樣,使用yaml或json格式,可採用字典或列表的形式。
---
foo:
a: hello
b: world
playbook文件如下,使用vars_files指令來引入:
---
- name: play1
hosts: node1
gather_facts: no
vars_files:
- varfile.yaml # 多個變量文件,使用列表形式即可
tasks:
- name: task1
debug:
msg: "{{foo.a}} {{foo.b}}"
include_vars模塊
include_vars是模塊提供的功能,它是一個手動創建的任務,和shell、debug等模塊一樣。所以只有當任務執行完後,相關變量才會創建。
下面介紹幾個用法。
引入一個文件:
---
- name: play1
hosts: node1
gather_facts: no
vars_files:
- varfile.yaml
tasks:
- name: task1
include_vars: varfile.yaml # 引入之後,可在後續的任務中使用
- name: task2
debug:
msg: "{{foo.a}} {{foo.b}}"
引入多個文件,可採用循環:
- name: task1
include_vars: "{{ item }}"
loop:
- varfile1.yaml
- varfile2.yaml
還可以引入目錄,使用條件和其它參數控制引入的變量文件。這裡不展開了,該模塊有很多參數和用法,具體可參考官網:
-e選項
ansible-playbook命令的-e選項或–extra-vars選項也可以用來定義變量或引入變量文件:
# 定義單個變量
$ ansible-playbook -e 'var1="value1"' xxx.yml
# 定義多個變量
$ ansible-playbook -e 'var1="value1" var2="value2"' xxx.yml
# 引入單個變量文件
$ ansible-playbook -e '@varfile1.yml' xxx.yml
# 引入多個變量文件
$ ansible-playbook -e '@varfile1.yml' -e '@varfile2.yml' xxx.yml
使用role
上面將各類內容放在單獨的文件中,然後使用相關指令或模塊將其引入。ansible中,有一種更為規範的組織方式,即role
。
使用role
,即可無需手動使用這些指令或模塊了。按照role指定的文件或目錄存放對應的內容,ansible就會自動引入。
role的文件結構
ansible-galaxy init role1
命令,可以快速創建一個role
框架。
$ cd /etc/ansible/roles
$ ansible-galaxy init role01
$ tree role01
role01
├── defaults
│ └── main.yml
├── files # 外部文件,放入此處的文件,在role的各種任務中直接無需使用全路徑
├── handlers
│ └── main.yml # 存放handler
├── meta
│ └── main.yml # 該role依賴的先行role。定義在此處的role將在該role運行前執行
├── README.md
├── tasks
│ └── main.yml # 存放任務
├── templates # 模板文件,放入此處的文件,在role的各種任務中無需使用全路徑
├── tests
│ ├── inventory
│ └── test.yml
└── vars
└── main.yml # 存放變量
在相應目錄及文件下編寫對應內容,然後還需要提供一個入口playbook文件。在入口playbook文件中,使用import_roles
、include_role
、roles
指令來引入role。最後使用ansible-playbook命令執行入口文件,即可執行定義在role中的各種任務了。
enter.yml文件如下:
---
- name: enter for all roles
hosts: node1
gather_facts: no
roles:
- role01 # 多個role可使用列表一起列出
編寫一個role
定義role的變量
etc/ansible/roles/role01/vars/main.yml
文件是role定義變量或引入變量文件的地方。
etc/ansible/roles/role01/defaults/main.yml
文件是role定義默認變量的地方,優先級較低,當然也可以引入文件。
---
# vars file for role01
foo1:
a: hello
b: world
定義role的task
/etc/ansible/roles/role01/tasks/main.yml
文件是role定義task或者引入task文件的地方。
---
# tasks file for role01
- name: task1
debug:
msg: "{{ foo1.a }} {{ foo1.b }}"
notify: handler1
changed_when: true # 該指令使得chenged=1,觸發notify
定義role的handler
/etc/ansible/roles/role01/handles/main.yml
文件是role定義handler或者引入handler文件的地方。
---
# handlers file for role01
- name: handler1
debug:
msg: "run handler1"
執行:
$ ansible-playbook enter.yml
PLAY [enter for all roles] **********************************************************************************************************
TASK [role01 : task1] ***************************************************************************************************************
changed: [node1] => {}
MSG:
hello world
RUNNING HANDLER [role01 : handler1] *************************************************************************************************
ok: [node1] => {}
MSG:
run handler1
PLAY RECAP **************************************************************************************************************************
node1 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
playbook的執行順序
一個節點執行任務的順序如下:
- 解析配置/etc/ansible/ansible.cfg
- 解析inventory
- gather_facts任務
- pre_tasks任務
- pre_tasks任務觸發的handler
- roles指令加載的role
- task指令中的任務
- roles和tasks觸發的handler
- post_tasks指令中的任務
- post_tasks中任務觸發的handler
多個節點時,ansible在所有節點上執行完成前一個任務後,才進入下一個任務的執行流程。handlers是在所有節點上的所有任務執行完成後,開始執行。
- 從inventory中選擇執行play的相關主機
- 連接到遠程主機,通常使用ssh方式
- 拷貝相關模塊到遠程主機,並開始執行
理解委託任務
委託是指將原本在一個節點上執行的任務,委託給另一個節點執行。
要想理解委託,需要先了解ansible任務的執行過程:默認情況下,ansible先選擇執行任務的主機,後連接該主機,再拷貝相關模塊,並在該遠程主機上執行模塊。
但是在任務中進行了委託後,實際連接主機和執行模塊動作將發生改變。比如將node1主機的任務委託給node2,ansible會根據hosts指令選擇遠程主機node1後,然後根據delegate_to
指令,連接到node2節點,並將相關模塊拷貝到node2並在node2上執行。
以下使用dalegate_to
指令做個演示
test.yaml文件:
---
- name: play1
hosts: node1
gather_facts: no
tasks:
- name: task1
shell: hostname
delegate_to: node2 # 委託node2執行shell模塊
執行ansible-playbook -vvv test.yaml
,然後查看具體的執行日誌。
再將delegate_to
指令去除,執行ansible-playbook -vvv test.yaml
命令查看不委託的執行過程,兩者對比很容易發現ansible連接的主機為委託的主機。
$ ansible-playbook -vvv test.yaml
打印輸出日誌如下,也可在/var/log/ansible.log
中查看。
ansible-playbook 2.9.25
config file = /etc/ansible/ansible.cfg
configured module search path = [u'/root/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
ansible python module location = /usr/lib/python2.7/site-packages/ansible
executable location = /usr/bin/ansible-playbook
python version = 2.7.5 (default, Oct 14 2020, 14:45:30) [GCC 4.8.5 20150623 (Red Hat 4.8.5-44)]
Using /etc/ansible/ansible.cfg as config file
host_list declined parsing /etc/ansible/hosts as it did not pass its verify_file() method
script declined parsing /etc/ansible/hosts as it did not pass its verify_file() method
auto declined parsing /etc/ansible/hosts as it did not pass its verify_file() method
Parsed /etc/ansible/hosts inventory source with ini plugin
Skipping callback 'actionable', as we already have a stdout callback.
Skipping callback 'counter_enabled', as we already have a stdout callback.
Skipping callback 'debug', as we already have a stdout callback.
Skipping callback 'dense', as we already have a stdout callback.
Skipping callback 'dense', as we already have a stdout callback.
Skipping callback 'full_skip', as we already have a stdout callback.
Skipping callback 'json', as we already have a stdout callback.
Skipping callback 'minimal', as we already have a stdout callback.
Skipping callback 'null', as we already have a stdout callback.
Skipping callback 'oneline', as we already have a stdout callback.
Skipping callback 'selective', as we already have a stdout callback.
Skipping callback 'skippy', as we already have a stdout callback.
Skipping callback 'stderr', as we already have a stdout callback.
Skipping callback 'unixy', as we already have a stdout callback.
Skipping callback 'yaml', as we already have a stdout callback.
PLAYBOOK: test.yaml *****************************************************************************************************************
1 plays in test.yaml
PLAY [play1] ************************************************************************************************************************
META: ran handlers
TASK [task1] ************************************************************************************************************************
task path: /etc/ansible/test.yaml:6
<node2> ESTABLISH SSH CONNECTION FOR USER: None
<node2> SSH: EXEC ssh -C -o ControlMaster=auto -o ControlPersist=60s -o Port=22 -o KbdInteractiveAuthentication=no -o PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey -o PasswordAuthentication=no -o ConnectTimeout=10 -o ControlPath=/root/.ansible/cp/0d67807d8c node2 '/bin/sh -c '"'"'echo ~ && sleep 0'"'"''
<node2> (0, '/root\n', '')
<node2> ESTABLISH SSH CONNECTION FOR USER: None
<node2> SSH: EXEC ssh -C -o ControlMaster=auto -o ControlPersist=60s -o Port=22 -o KbdInteractiveAuthentication=no -o PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey -o PasswordAuthentication=no -o ConnectTimeout=10 -o ControlPath=/root/.ansible/cp/0d67807d8c node2 '/bin/sh -c '"'"'( umask 77 && mkdir -p "` echo /root/.ansible/tmp `"&& mkdir "` echo /root/.ansible/tmp/ansible-tmp-1642857067.57-71643-202799162287554 `" && echo ansible-tmp-1642857067.57-71643-202799162287554="` echo /root/.ansible/tmp/ansible-tmp-1642857067.57-71643-202799162287554 `" ) && sleep 0'"'"''
<node2> (0, 'ansible-tmp-1642857067.57-71643-202799162287554=/root/.ansible/tmp/ansible-tmp-1642857067.57-71643-202799162287554\n', '')
<node1> Attempting python interpreter discovery
<node2> ESTABLISH SSH CONNECTION FOR USER: None
<node2> SSH: EXEC ssh -C -o ControlMaster=auto -o ControlPersist=60s -o Port=22 -o KbdInteractiveAuthentication=no -o PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey -o PasswordAuthentication=no -o ConnectTimeout=10 -o ControlPath=/root/.ansible/cp/0d67807d8c node2 '/bin/sh -c '"'"'echo PLATFORM; uname; echo FOUND; command -v '"'"'"'"'"'"'"'"'/usr/bin/python'"'"'"'"'"'"'"'"'; command -v '"'"'"'"'"'"'"'"'python3.7'"'"'"'"'"'"'"'"'; command -v '"'"'"'"'"'"'"'"'python3.6'"'"'"'"'"'"'"'"'; command -v '"'"'"'"'"'"'"'"'python3.5'"'"'"'"'"'"'"'"'; command -v '"'"'"'"'"'"'"'"'python2.7'"'"'"'"'"'"'"'"'; command -v '"'"'"'"'"'"'"'"'python2.6'"'"'"'"'"'"'"'"'; command -v '"'"'"'"'"'"'"'"'/usr/libexec/platform-python'"'"'"'"'"'"'"'"'; command -v '"'"'"'"'"'"'"'"'/usr/bin/python3'"'"'"'"'"'"'"'"'; command -v '"'"'"'"'"'"'"'"'python'"'"'"'"'"'"'"'"'; echo ENDFOUND && sleep 0'"'"''
<node2> (0, 'PLATFORM\nLinux\nFOUND\n/usr/bin/python\n/usr/bin/python2.7\n/usr/libexec/platform-python\n/usr/bin/python\nENDFOUND\n', '')
<node2> ESTABLISH SSH CONNECTION FOR USER: None
<node2> SSH: EXEC ssh -C -o ControlMaster=auto -o ControlPersist=60s -o Port=22 -o KbdInteractiveAuthentication=no -o PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey -o PasswordAuthentication=no -o ConnectTimeout=10 -o ControlPath=/root/.ansible/cp/0d67807d8c node2 '/bin/sh -c '"'"'/usr/bin/python && sleep 0'"'"''
<node2> (0, '{"osrelease_content": "NAME=\\"CentOS Linux\\"\\nVERSION=\\"7 (Core)\\"\\nID=\\"centos\\"\\nID_LIKE=\\"rhel fedora\\"\\nVERSION_ID=\\"7\\"\\nPRETTY_NAME=\\"CentOS Linux 7 (Core)\\"\\nANSI_COLOR=\\"0;31\\"\\nCPE_NAME=\\"cpe:/o:centos:centos:7\\"\\nHOME_URL=\\"//www.centos.org/\\"\\nBUG_REPORT_URL=\\"//bugs.centos.org/\\"\\n\\nCENTOS_MANTISBT_PROJECT=\\"CentOS-7\\"\\nCENTOS_MANTISBT_PROJECT_VERSION=\\"7\\"\\nREDHAT_SUPPORT_PRODUCT=\\"centos\\"\\nREDHAT_SUPPORT_PRODUCT_VERSION=\\"7\\"\\n\\n", "platform_dist_result": ["centos", "7.9.2009", "Core"]}\n', '')
Using module file /usr/lib/python2.7/site-packages/ansible/modules/commands/command.py
<node2> PUT /root/.ansible/tmp/ansible-local-71634Zeeqqp/tmpWHppzz TO /root/.ansible/tmp/ansible-tmp-1642857067.57-71643-202799162287554/AnsiballZ_command.py
<node2> SSH: EXEC sftp -b - -C -o ControlMaster=auto -o ControlPersist=60s -o Port=22 -o KbdInteractiveAuthentication=no -o PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey -o PasswordAuthentication=no -o ConnectTimeout=10 -o ControlPath=/root/.ansible/cp/0d67807d8c '[node2]'
<node2> (0, 'sftp> put /root/.ansible/tmp/ansible-local-71634Zeeqqp/tmpWHppzz /root/.ansible/tmp/ansible-tmp-1642857067.57-71643-202799162287554/AnsiballZ_command.py\n', '')
<node2> ESTABLISH SSH CONNECTION FOR USER: None
<node2> SSH: EXEC ssh -C -o ControlMaster=auto -o ControlPersist=60s -o Port=22 -o KbdInteractiveAuthentication=no -o PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey -o PasswordAuthentication=no -o ConnectTimeout=10 -o ControlPath=/root/.ansible/cp/0d67807d8c node2 '/bin/sh -c '"'"'chmod u+x /root/.ansible/tmp/ansible-tmp-1642857067.57-71643-202799162287554/ /root/.ansible/tmp/ansible-tmp-1642857067.57-71643-202799162287554/AnsiballZ_command.py && sleep 0'"'"''
<node2> (0, '', '')
<node2> ESTABLISH SSH CONNECTION FOR USER: None
<node2> SSH: EXEC ssh -C -o ControlMaster=auto -o ControlPersist=60s -o Port=22 -o KbdInteractiveAuthentication=no -o PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey -o PasswordAuthentication=no -o ConnectTimeout=10 -o ControlPath=/root/.ansible/cp/0d67807d8c -tt node2 '/bin/sh -c '"'"'/usr/bin/python /root/.ansible/tmp/ansible-tmp-1642857067.57-71643-202799162287554/AnsiballZ_command.py && sleep 0'"'"''
<node2> (0, '\r\n{"changed": true, "end": "2022-01-22 21:11:09.433351", "stdout": "node2", "cmd": "hostname", "rc": 0, "start": "2022-01-22 21:11:09.333195", "stderr": "", "delta": "0:00:00.100156", "invocation": {"module_args": {"creates": null, "executable": null, "_uses_shell": true, "strip_empty_ends": true, "_raw_params": "hostname", "removes": null, "argv": null, "warn": true, "chdir": null, "stdin_add_newline": true, "stdin": null}}}\r\n', 'Shared connection to node2 closed.\r\n')
<node2> ESTABLISH SSH CONNECTION FOR USER: None
<node2> SSH: EXEC ssh -C -o ControlMaster=auto -o ControlPersist=60s -o Port=22 -o KbdInteractiveAuthentication=no -o PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey -o PasswordAuthentication=no -o ConnectTimeout=10 -o ControlPath=/root/.ansible/cp/0d67807d8c node2 '/bin/sh -c '"'"'rm -f -r /root/.ansible/tmp/ansible-tmp-1642857067.57-71643-202799162287554/ > /dev/null 2>&1 && sleep 0'"'"''
<node2> (0, '', '')
changed: [node1 -> node2] => {
"changed": true,
"cmd": "hostname",
"delta": "0:00:00.100156",
"end": "2022-01-22 21:11:09.433351",
"invocation": {
"module_args": {
"_raw_params": "hostname",
"_uses_shell": true,
"argv": null,
"chdir": null,
"creates": null,
"executable": null,
"removes": null,
"stdin": null,
"stdin_add_newline": true,
"strip_empty_ends": true,
"warn": true
}
},
"rc": 0,
"start": "2022-01-22 21:11:09.333195"
}
STDOUT:
node2
META: ran handlers
META: ran handlers
PLAY RECAP **************************************************************************************************************************
node1 : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
到此為止,ansible的基礎算是過了一遍,對ansible的基礎概念、用法有了一個大概的了解。
但是ansible的內容和細節還有很多,包括不限於:
-
inventory更加複雜的定義方式
-
各種級別的指令
-
眾多類型的變量和其作用
-
各種條件判斷
-
各類循環,以及其它控制流程
-
文件如何加載解析、任務執行的順序及方式
-
更多的模塊、插件
-
role、vault、jinja2
-
二次開發
-
配置文件